Skip to content

Commit b2b41dc

Browse files
montfortclaude
andauthored
fix(cli): cli-3.15.0 — idempotent injection + host marker-health validation (#162)
Fixes the duplicated `<!-- straymark:end -->` accumulation in `.cursorrules` and other host files. Root cause: STRAYMARK.md's own marker-convention docs contain the literal marker strings inside a fenced code block, so the old `find(MARKER_END)` stopped at the in-docs literal and dropped the real END outside the canonical block as an orphan on every update-framework run. - `find_canonical_block` switches to first-BEGIN/last-END (`rfind`) so the canonical span survives marker literals embedded in the payload - `sanitize_orphan_markers` auto-repairs files with orphan markers or pathological double-block corruption on the next re-injection - `inject::MarkerHealth` + `inject::inspect_marker_health` diagnostic API - `straymark validate` emits `host-marker-health` warnings for malformed host files (rule walks `manifest.injections`) - 10 new tests in `inject.rs::tests` cover orphan-end-before-begin, two complete blocks, orphan-begin-no-end, idempotency, end-to-end repair, healthy-with-marker-literals-in-embed, and Sentinel-style extra END - `doc_count == 0` no longer suppresses host-marker warnings Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 68cd86c commit b2b41dc

11 files changed

Lines changed: 556 additions & 36 deletions

File tree

CHANGELOG.md

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

88
---
99

10+
## CLI 3.15.0 — Idempotent injection + host marker-health validation
11+
12+
Fixes a long-standing bug in `cli/src/inject.rs::replace_between_markers` that left a duplicated `<!-- straymark:end -->` in host files such as `.cursorrules`, `CLAUDE.md`, `AGENTS.md`, `GEMINI.md`, `.cursor/rules/straymark.md`, and `.github/copilot-instructions.md` after every `straymark update-framework`. The root cause was two-fold: (1) the old code called `content.find(MARKER_BEGIN)` and `content.find(MARKER_END)` independently and never verified `start < end`; (2) more critically, `.cursorrules` and `.cursor/rules/straymark.md` embed `STRAYMARK.md`, whose own documentation contains the literal strings `<!-- straymark:begin -->` and `<!-- straymark:end -->` inside a fenced code block that describes the marker convention. `find(MARKER_END)` stopped at the in-docs literal instead of the trailing real END, so every update truncated the canonical block mid-embed and dropped the real END outside it as an orphan — accumulating one extra `<!-- straymark:end -->` per update. The fix makes injection idempotent and auto-repairing, and adds a diagnostic signal to `straymark validate`.
13+
14+
### Fixed (CLI)
15+
16+
- **`inject::find_canonical_block` uses first-BEGIN/last-END semantics** (`cli/src/inject.rs`) — switched from `find(MARKER_END)` to `rfind(MARKER_END)`, so the canonical span runs from the first BEGIN in the file to the *last* END. This correctly skips marker literals embedded in the payload (the actual STRAYMARK.md describes the markers in its own docs) and survives the pathological "two complete blocks" corruption case by engulfing both pairs into a single replacement region. Trade-off documented in the function's docstring.
17+
- **`inject::replace_between_markers` / `inject::remove_between_markers`** (`cli/src/inject.rs`) — both route through a new `sanitize_orphan_markers` helper that preserves the canonical block intact and strips orphans (lone BEGIN/END markers outside the canonical span, or any markers when there is no canonical block). Lines containing only a marker are removed entirely; markers embedded inside lines with other content lose only the marker substring; consecutive blank lines created by the cleanup collapse to a single blank line. `inject_directive` now treats any marker (BEGIN or END) as a signal to take the replace-and-repair path, so a file containing only an orphan `<!-- straymark:end -->` is still repaired on the next update.
18+
- **Sentinel reproduction covered by tests** — six new tests in `cli/src/inject.rs::tests` exercise orphan-end-before-begin, two complete blocks, orphan-begin-no-end, idempotency across two consecutive passes, end-to-end `.cursorrules` repair via the public `inject_directive` API, and the orphan-end-only-file case. The existing `test_replace_between_markers` was hardened to assert the output contains exactly one BEGIN and one END.
19+
20+
### Added (CLI)
21+
22+
- **`inject::MarkerHealth` + `inject::inspect_marker_health`** (`cli/src/inject.rs`) — diagnostic API that reports a host file's marker structure without mutating it: `begin_count`, `end_count`, `has_canonical_block`, and `end_before_begin`. `is_malformed()` returns true on asymmetric counts (the Sentinel case shows up as `end_count` one greater than `begin_count`), markers-without-a-canonical-block, or end-before-begin inversions. Five new tests cover healthy / no-markers / healthy-with-marker-literals-in-embed (the `.cursorrules` reality) / end-before-begin / extra-orphan-end (Sentinel) / orphan-begin cases.
23+
- **`straymark validate` host marker-health warning** (`cli/src/commands/validate.rs`) — new `check_host_marker_health` walks `manifest.injections` from the local `.straymark/dist-manifest.yml`, calls `inject::inspect_marker_health` on every declared target that exists, and emits a `ValidationWarning` (rule `host-marker-health`) when the structure is malformed. Message names the specific issue (asymmetric counts, no-canonical-block, end-before-begin) and points the operator at `straymark update-framework` or `straymark repair` to auto-repair. Merges into the existing `ValidationResult` flow so the `--fix` path and re-validation also pick it up. If the manifest cannot be loaded, the check is silently skipped.
24+
- **`doc_count == 0` no longer suppresses host-marker warnings** — when a project has no `.straymark/` documents but a host file has malformed markers, the warning still surfaces. The "no documents found" hint only fires when the validation result is empty.
25+
26+
### Adopter guidance
27+
28+
Run `straymark update-cli` to install `cli-3.15.0`. The next `straymark update-framework` (whenever a new framework release lands) auto-repairs any `.cursorrules` or other host file that accumulated a duplicate end marker — no manual editing required. Adopters who want a heads-up before re-running the update can call `straymark validate` now; malformed host files appear in the warning summary under the `host-marker-health` rule.
29+
30+
The Sentinel `.cursorrules` duplication observed after a recent `straymark update-framework` resolves automatically on the next framework update once `cli-3.15.0` is in place. No framework bump is required — `fw-4.17.0` from the previous release cycle remains current.
31+
32+
---
33+
1034
## Framework 4.17.0 — Emergent-observation design meta-pattern
1135

1236
Names a design property of the StrayMark framework that was already present but unnamed: *formal cross-referencing (frontmatter linkage, canonical sections, stable IDs) composed with cultural permission to surface beyond the asked task* produces agents that, while reading only the framework documentation, surface dissonance between canonical sources. This composition produced the Sentinel observation that crystallized as `CHARTER-CHAIN-EVOLUTION.md` Pattern 1 in `fw-4.16.0`. The new doc and a new Principle codify the meta so it can be preserved deliberately, and lists four open application axes (MCARD ↔ deployed model, SBOM ↔ lockfiles, ADR ↔ contradicting implementation, Constitution Check ↔ framework bump) as candidates for future N=1-validated extension. No CLI bump.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ StrayMark uses independent version tags for each component:
277277
| Component | Tag prefix | Example | Includes |
278278
| --- | --- | --- | --- |
279279
| Framework | `fw-` | `fw-4.17.0` | Templates (12 types), governance, directives, Charter template + schema |
280-
| CLI | `cli-` | `cli-3.14.1` | The `straymark` binary |
280+
| CLI | `cli-` | `cli-3.15.0` | The `straymark` binary |
281281

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

cli/Cargo.lock

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

cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "straymark-cli"
3-
version = "3.14.1"
3+
version = "3.15.0"
44
edition = "2021"
55
description = "CLI for StrayMark — the cognitive discipline your AI-assisted projects need"
66
license = "MIT"

cli/src/commands/validate.rs

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
use anyhow::{bail, Result};
22
use colored::Colorize;
33
use std::collections::BTreeMap;
4-
use std::path::PathBuf;
4+
use std::path::{Path, PathBuf};
55

6+
use crate::inject;
7+
use crate::manifest::DistManifest;
68
use crate::utils;
79
use crate::validation::{self, Severity, ValidationIssue};
810

@@ -68,7 +70,11 @@ pub fn run(
6870
}
6971
}
7072

71-
if doc_count == 0 {
73+
for issue in check_host_marker_health(&target, &straymark_dir) {
74+
result.warnings.push(issue);
75+
}
76+
77+
if doc_count == 0 && result.errors.is_empty() && result.warnings.is_empty() {
7278
utils::info("No documents found to validate.");
7379
println!(
7480
" {} Create documents with {} or {}",
@@ -96,6 +102,9 @@ pub fn run(
96102
result.warnings.push(issue);
97103
}
98104
}
105+
for issue in check_host_marker_health(&target, &straymark_dir) {
106+
result.warnings.push(issue);
107+
}
99108
print_results(&result, doc_count);
100109
return exit_with_code(&result);
101110
}
@@ -259,3 +268,60 @@ fn exit_with_code(result: &validation::ValidationResult) -> Result<()> {
259268
std::process::exit(1);
260269
}
261270
}
271+
272+
/// Inspect every injection target declared in the local `dist-manifest.yml` and
273+
/// emit a warning for each host file (`.cursorrules`, `CLAUDE.md`, etc.) whose
274+
/// StrayMark marker block is malformed (duplicated, orphan, or inverted).
275+
///
276+
/// Reparation is the job of `straymark update-framework` / `repair` (which call
277+
/// `inject::inject_directive` and auto-sanitize). This check is purely diagnostic.
278+
///
279+
/// If the manifest cannot be loaded (missing or malformed), the check is silently
280+
/// skipped — a corrupt manifest is reported elsewhere and we don't want to
281+
/// double-fail or block `validate` on it.
282+
fn check_host_marker_health(project_root: &Path, straymark_dir: &Path) -> Vec<ValidationIssue> {
283+
let manifest_path = straymark_dir.join("dist-manifest.yml");
284+
let manifest = match DistManifest::load(&manifest_path) {
285+
Ok(m) => m,
286+
Err(_) => return Vec::new(),
287+
};
288+
289+
let mut issues = Vec::new();
290+
for injection in &manifest.injections {
291+
let target = project_root.join(&injection.target);
292+
if !target.exists() {
293+
continue;
294+
}
295+
let health = match inject::inspect_marker_health(&target) {
296+
Ok(h) => h,
297+
Err(_) => continue,
298+
};
299+
if !health.is_malformed() {
300+
continue;
301+
}
302+
let mut parts = Vec::new();
303+
if health.begin_count != health.end_count {
304+
parts.push(format!(
305+
"{} begin / {} end marker(s) (counts must match)",
306+
health.begin_count, health.end_count
307+
));
308+
}
309+
if !health.has_canonical_block && (health.begin_count > 0 || health.end_count > 0) {
310+
parts.push("no canonical block (only orphan markers)".to_string());
311+
}
312+
if health.end_before_begin {
313+
parts.push("end marker before begin marker".to_string());
314+
}
315+
let detail = parts.join(", ");
316+
issues.push(ValidationIssue {
317+
file: target,
318+
rule: "host-marker-health".to_string(),
319+
message: format!("Malformed StrayMark markers ({detail})."),
320+
severity: Severity::Warning,
321+
fix_hint: Some(
322+
"Run 'straymark update-framework' or 'straymark repair' to auto-repair.".to_string(),
323+
),
324+
});
325+
}
326+
issues
327+
}

0 commit comments

Comments
 (0)