Skip to content

Commit 0f0b075

Browse files
montfortclaude
andauthored
feat(core): cli-3.23.1 — Loom M0: extract straymark-core (workspace + shared document model + typed graph) (#239)
CHARTER-01-loom-server M0 (T0.1–T0.7). Zero behavior change, verified by the declared regression oracle: 635 tests pass and `straymark audit` output is byte-for-byte identical pre/post refactor (JSON, cyclic-corpus JSON, text). - Root Cargo workspace (core + cli); [profile.release] and Cargo.lock at root - New straymark-core crate: document.rs moved verbatim; new core::graph — typed (RELATED_TO/SUPERSEDES/DOCUMENTS_ALTERNATIVE/CHANGES_API/ ORIGINATES_FROM), bidirectional, orphan-preserving, dangling refs kept as resolved:false edges (Spec 001 §3) - audit_engine::build_traceability is now a projection over the shared graph - Additive Frontmatter fields: supersedes, alternatives_documented, originating_ailogs - release-cli.yml: workspace target/ paths; publish straymark-core before cli - Lands the Loom intention docs (experimento/, 2 dogfood ADRs); Charter → in-progress; AILOG-2026-06-12-001 with Batch Ledger Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 14bd09b commit 0f0b075

43 files changed

Lines changed: 7150 additions & 81 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/release-cli.yml

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ jobs:
9797
if: matrix.archive == 'tar.gz'
9898
shell: bash
9999
run: |
100-
BINARY=cli/target/${{ matrix.target }}/release/straymark
100+
# Workspace root target/ since Loom M0 (cli/ is a workspace member)
101+
BINARY=target/${{ matrix.target }}/release/straymark
101102
ARCHIVE=straymark-cli-v${{ needs.resolve-version.outputs.version }}-${{ matrix.target }}.tar.gz
102103
tar czf "$ARCHIVE" -C "$(dirname "$BINARY")" "$(basename "$BINARY")"
103104
echo "ARCHIVE=$ARCHIVE" >> "$GITHUB_ENV"
@@ -106,7 +107,8 @@ jobs:
106107
if: matrix.archive == 'zip'
107108
shell: bash
108109
run: |
109-
BINARY=cli/target/${{ matrix.target }}/release/straymark.exe
110+
# Workspace root target/ since Loom M0 (cli/ is a workspace member)
111+
BINARY=target/${{ matrix.target }}/release/straymark.exe
110112
ARCHIVE=straymark-cli-v${{ needs.resolve-version.outputs.version }}-${{ matrix.target }}.zip
111113
cp "$BINARY" straymark.exe
112114
7z a "$ARCHIVE" straymark.exe
@@ -181,7 +183,21 @@ jobs:
181183
- name: Install Rust toolchain
182184
uses: dtolnay/rust-toolchain@stable
183185

184-
- name: Publish to crates.io
186+
- name: Publish straymark-core (if version not yet on crates.io)
187+
env:
188+
CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
189+
shell: bash
190+
run: |
191+
# straymark-cli depends on straymark-core by version; the core crate
192+
# must exist on crates.io before the CLI can be published.
193+
CORE_VERSION=$(grep '^version' core/Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
194+
if curl -fsSL "https://crates.io/api/v1/crates/straymark-core/$CORE_VERSION" | grep -q '"num"'; then
195+
echo "straymark-core $CORE_VERSION already published — skipping"
196+
else
197+
cargo publish --manifest-path core/Cargo.toml --allow-dirty
198+
fi
199+
200+
- name: Publish straymark-cli to crates.io
185201
env:
186202
CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
187203
run: cargo publish --manifest-path cli/Cargo.toml --allow-dirty

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ yarn.lock
9494
# CLI Build Artifacts
9595
# =============================================================================
9696

97+
/target/
9798
cli/target/
9899
dist-staging/
99100
*.tar.gz

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,22 @@ and this project uses [independent versioning](README.md#versioning) for Framewo
77

88
---
99

10+
## CLI 3.23.1 — `straymark-core` extraction (Loom M0)
11+
12+
First milestone of the experimental **Loom** component (`experimento/`, `CHARTER-01-loom-server`): the document model and traceability graph move into a shared crate so the CLI and the upcoming Loom visualization server parse StrayMark documents with exactly the same code (`ADR-2026-06-02-001`). **No user-facing behavior changes** — the full test suite and the `straymark audit` output are byte-for-byte identical pre/post refactor.
13+
14+
### Added (CLI)
15+
16+
- **`straymark-core` crate** (published to crates.io): `core::document` (the document model, moved verbatim from `cli/src/document.rs`) and `core::graph` — a typed, bidirectional, orphan-preserving knowledge graph over frontmatter cross-links (`related`, `supersedes`, `alternatives_documented`, `api_changes`, `originating_ailogs`), with dangling references kept as first-class `resolved: false` edges (Loom Spec 001 §3).
17+
- New optional frontmatter fields parsed (additive): `supersedes`, `alternatives_documented`, `originating_ailogs`.
18+
19+
### Changed (CLI)
20+
21+
- The repository root is now a Cargo **workspace** (`core` + `cli`); `[profile.release]` and `Cargo.lock` live at the root. Release binaries are built from `target/` (workspace) instead of `cli/target/``release-cli.yml` adjusted accordingly, and it now publishes `straymark-core` before `straymark-cli`.
22+
- `audit_engine::build_traceability` is now a projection over `straymark_core::graph` (same output, one graph builder for all consumers).
23+
24+
---
25+
1026
## Framework 4.26.0 / CLI 3.23.0 — `charter drift` is native Rust (Windows-native parity)
1127

1228
Closes the last functional Windows-native gap ([#237](https://github.com/StrangeDaysTech/straymark/issues/237)): `straymark charter drift` no longer delegates to a bash script, so it runs without WSL or Git Bash.

CLAUDE.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
# StrayMark — Development Instructions
22

3-
This is the StrayMark project repository. It contains two main components:
3+
This is the StrayMark project repository. It contains two main components, plus one experimental:
44

55
- **Framework** (`dist/`): documentation templates, governance policies, and agent directives
66
- **CLI** (`cli/`): the `straymark` Rust binary that manages the framework in user projects
7+
- **Loom** (`experimento/`, EXPERIMENTAL): knowledge-graph/architecture visualization server — see `experimento/README.md` and `experimento/CHARTER-01-loom-server.md`
8+
9+
The Rust code is a **Cargo workspace** (root `Cargo.toml`, members `core` + `cli`): `straymark-core` holds the shared document model and knowledge graph (one parser for the CLI and Loom); `Cargo.lock` and `[profile.release]` live at the workspace root; build artifacts land in the root `target/`.
710

811
## Project Structure
912

1013
```
1114
straymark/
15+
├── Cargo.toml # Workspace root (members: core, cli) + release profile
16+
├── Cargo.lock
17+
├── core/ # straymark-core: shared document model + typed knowledge graph
18+
│ └── src/
19+
│ ├── document.rs # DocType, Frontmatter, parse_document, discover_documents
20+
│ └── graph.rs # Typed, bidirectional, orphan-preserving graph builder
1221
├── cli/ # Rust CLI source code
1322
│ ├── src/
1423
│ │ ├── main.rs # Entry point, command routing
@@ -23,8 +32,7 @@ straymark/
2332
│ │ ├── self_update.rs # CLI auto-update
2433
│ │ └── utils.rs # Output helpers, file hashing
2534
│ ├── tests/ # Integration tests
26-
│ ├── Cargo.toml
27-
│ └── Cargo.lock
35+
│ └── Cargo.toml
2836
├── dist/ # Framework distribution files
2937
│ ├── .straymark/ # Templates, governance, config
3038
│ ├── STRAYMARK.md # Unified governance rules
@@ -59,7 +67,7 @@ Edit `cli/Cargo.toml`:
5967
version = "X.Y.Z"
6068
```
6169

62-
Run `cargo check` in `cli/` to update `Cargo.lock`.
70+
Run `cargo check` at the repo root to update the workspace `Cargo.lock`.
6371

6472
Update version references in all docs that mention version numbers:
6573
- `docs/adopters/CLI-REFERENCE.md` (EN — versioning table + example outputs)
@@ -78,7 +86,7 @@ Update `CHANGELOG.md` (root) following [Keep a Changelog](https://keepachangelog
7886

7987
```bash
8088
git checkout -b chore/bump-cli-X.Y.Z
81-
git add cli/Cargo.toml cli/Cargo.lock docs/
89+
git add cli/Cargo.toml Cargo.lock docs/
8290
git commit -m "chore: bump CLI version to X.Y.Z"
8391
# Push, create PR, merge to main
8492
```

cli/Cargo.lock renamed to Cargo.lock

Lines changed: 11 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[workspace]
2+
resolver = "2"
3+
members = ["core", "cli"]
4+
5+
# Shared release profile (moved from cli/Cargo.toml when the workspace was
6+
# introduced in Loom M0 — profiles are only honored at the workspace root).
7+
[profile.release]
8+
opt-level = "z"
9+
lto = true
10+
strip = true

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ StrayMark uses independent version tags for each component:
278278
| Component | Tag prefix | Example | Includes |
279279
| --- | --- | --- | --- |
280280
| Framework | `fw-` | `fw-4.26.0` | Templates (12 types), governance, directives, Charter template + schema |
281-
| CLI | `cli-` | `cli-3.23.0` | The `straymark` binary |
281+
| CLI | `cli-` | `cli-3.23.1` | The `straymark` binary |
282282

283283
Check installed versions with `straymark status` or `straymark about`.
284284

cli/Cargo.toml

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "straymark-cli"
3-
version = "3.23.0"
3+
version = "3.23.1"
44
edition = "2021"
55
description = "CLI for StrayMark — the cognitive discipline your AI-assisted projects need"
66
license = "MIT"
@@ -10,13 +10,14 @@ readme = "README.md"
1010
keywords = ["straymark", "documentation", "governance", "ai", "cli"]
1111
categories = ["command-line-utilities", "development-tools"]
1212
authors = ["Strange Days Tech, S.A.S."]
13-
include = ["src/**/*", "Cargo.toml", "Cargo.lock", "README.md"]
13+
include = ["src/**/*", "Cargo.toml", "README.md"]
1414

1515
[[bin]]
1616
name = "straymark"
1717
path = "src/main.rs"
1818

1919
[dependencies]
20+
straymark-core = { version = "0.1.0", path = "../core" }
2021
clap = { version = "4", features = ["derive"] }
2122
reqwest = { version = "0.12", features = ["blocking", "rustls-tls", "json"] }
2223
serde = { version = "1", features = ["derive"] }
@@ -52,7 +53,5 @@ assert_cmd = "2"
5253
predicates = "3"
5354
tempfile = "3"
5455

55-
[profile.release]
56-
opt-level = "z"
57-
lto = true
58-
strip = true
56+
# [profile.release] moved to the workspace root Cargo.toml (Loom M0) —
57+
# cargo only honors profiles declared at the workspace root.

cli/src/audit_engine.rs

Lines changed: 41 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ use chrono::NaiveDate;
44
use serde::Serialize;
55

66
use crate::compliance::{self, ComplianceReport};
7-
use crate::document::StrayMarkDocument;
7+
use straymark_core::document::StrayMarkDocument;
8+
use straymark_core::graph::{EdgeType, Graph};
89

910
/// A single entry in the audit timeline
1011
#[derive(Debug, Clone, Serialize)]
@@ -135,35 +136,36 @@ fn build_timeline(docs: &[&StrayMarkDocument]) -> Vec<TimelineEntry> {
135136
entries
136137
}
137138

138-
/// Build traceability chains from document relationships
139+
/// Build traceability chains from document relationships.
140+
///
141+
/// Since Loom M0 this is a projection over the shared typed graph
142+
/// (`straymark_core::graph`): the legacy chain view keeps only resolved
143+
/// `RELATED_TO` edges between documents with explicit frontmatter ids, so the
144+
/// audit output is unchanged while the full graph (typed edges, orphans,
145+
/// dangling references, bidirectional adjacency) is available to other
146+
/// consumers (the Loom server).
139147
fn build_traceability(docs: &[&StrayMarkDocument]) -> Vec<TraceabilityChain> {
140-
// Build lookup by ID and by filename stem
141-
let mut doc_by_id: HashMap<String, &StrayMarkDocument> = HashMap::new();
142-
let mut referenced_ids: HashSet<String> = HashSet::new();
143-
144-
for doc in docs {
145-
if let Some(id) = &doc.frontmatter.id {
146-
doc_by_id.insert(id.clone(), doc);
147-
}
148-
}
148+
let graph = Graph::build(docs);
149149

150-
// Build adjacency: id -> list of ids it references
150+
// Legacy adjacency: id -> ids it references via resolved RELATED_TO edges,
151+
// both endpoints carrying explicit frontmatter ids.
151152
let mut adjacency: HashMap<String, Vec<String>> = HashMap::new();
152-
for doc in docs {
153-
if let Some(id) = &doc.frontmatter.id {
154-
if let Some(related) = &doc.frontmatter.related {
155-
let refs: Vec<String> = related
156-
.iter()
157-
.filter(|r| doc_by_id.contains_key(r.as_str()))
158-
.cloned()
159-
.collect();
160-
for r in &refs {
161-
referenced_ids.insert(r.clone());
162-
}
163-
if !refs.is_empty() {
164-
adjacency.insert(id.clone(), refs);
165-
}
166-
}
153+
let mut referenced_ids: HashSet<String> = HashSet::new();
154+
for node in &graph.nodes {
155+
if !node.has_explicit_id {
156+
continue;
157+
}
158+
let refs: Vec<String> = graph
159+
.out_edges(&node.id)
160+
.filter(|e| e.edge_type == EdgeType::RelatedTo && e.resolved)
161+
.filter(|e| graph.node(&e.target).is_some_and(|t| t.has_explicit_id))
162+
.map(|e| e.target.clone())
163+
.collect();
164+
for r in &refs {
165+
referenced_ids.insert(r.clone());
166+
}
167+
if !refs.is_empty() {
168+
adjacency.insert(node.id.clone(), refs);
167169
}
168170
}
169171

@@ -173,9 +175,11 @@ fn build_traceability(docs: &[&StrayMarkDocument]) -> Vec<TraceabilityChain> {
173175
}
174176

175177
// Find root nodes: documents that are not referenced by others
176-
let root_ids: Vec<String> = docs
178+
let root_ids: Vec<String> = graph
179+
.nodes
177180
.iter()
178-
.filter_map(|d| d.frontmatter.id.clone())
181+
.filter(|n| n.has_explicit_id)
182+
.map(|n| n.id.clone())
179183
.filter(|id| !referenced_ids.contains(id))
180184
.filter(|id| adjacency.contains_key(id))
181185
.collect();
@@ -189,19 +193,15 @@ fn build_traceability(docs: &[&StrayMarkDocument]) -> Vec<TraceabilityChain> {
189193
continue;
190194
}
191195

192-
let root_doc = match doc_by_id.get(root_id.as_str()) {
193-
Some(d) => d,
196+
let root_doc = match graph.node(root_id) {
197+
Some(n) => n,
194198
None => continue,
195199
};
196200

197201
let root_node = TraceabilityNode {
198202
id: root_id.clone(),
199-
doc_type: root_doc.doc_type.prefix().to_string(),
200-
title: root_doc
201-
.frontmatter
202-
.title
203-
.clone()
204-
.unwrap_or_else(|| "Untitled".into()),
203+
doc_type: root_doc.doc_type.clone(),
204+
title: root_doc.title.clone(),
205205
};
206206

207207
let mut chain_nodes = Vec::new();
@@ -226,15 +226,11 @@ fn build_traceability(docs: &[&StrayMarkDocument]) -> Vec<TraceabilityChain> {
226226
visited.insert(current_id.clone());
227227
globally_visited.insert(current_id.clone());
228228

229-
if let Some(doc) = doc_by_id.get(current_id.as_str()) {
229+
if let Some(node) = graph.node(&current_id) {
230230
chain_nodes.push(TraceabilityNode {
231231
id: current_id.clone(),
232-
doc_type: doc.doc_type.prefix().to_string(),
233-
title: doc
234-
.frontmatter
235-
.title
236-
.clone()
237-
.unwrap_or_else(|| "Untitled".into()),
232+
doc_type: node.doc_type.clone(),
233+
title: node.title.clone(),
238234
});
239235

240236
if let Some(refs) = adjacency.get(&current_id) {
@@ -333,7 +329,7 @@ pub fn generate_audit(
333329
#[cfg(test)]
334330
mod tests {
335331
use super::*;
336-
use crate::document::{DocType, Frontmatter};
332+
use straymark_core::document::{DocType, Frontmatter};
337333
use std::path::PathBuf;
338334

339335
fn make_doc(filename: &str, doc_type: DocType, fm: Frontmatter) -> StrayMarkDocument {

cli/src/commands/approve.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use chrono::Local;
1616
use colored::Colorize;
1717
use std::path::{Path, PathBuf};
1818

19-
use crate::document;
19+
use straymark_core::document;
2020
use crate::prompts;
2121
use crate::utils;
2222

0 commit comments

Comments
 (0)