Skip to content

Commit 6f46d30

Browse files
montfortclaude
andauthored
fix(framework+cli): fw-4.16.2 / cli-3.14.1 — straymark repair per-target restore (#159)
Issue #156 closure-comment follow-up. The CLI bug surfaced as: `straymark repair` ignored a single missing injection target (e.g. a deleted AGENTS.md) when STRAYMARK.md was still present. Both the download trigger and the inject path were gated on STRAYMARK.md being absent. From this release both gates are removed and `repair` mirrors `straymark update-framework`'s per-target behavior for files declared under `dist-manifest.yml::injections:`. CLI (cli-3.14.1): - `check_needs_download()` now reads the local dist-manifest.yml and flags the download as needed if any declared injection target is missing. - `restore_missing_files()` replaces the `if !straymark_md.exists()` gate with a per-target presence filter — each missing injection target loads its template from the ZIP and runs through `inject::inject_directive`. Targets already present are left untouched. - 5 new unit tests in `repair.rs::tests` cover `check_needs_download` branches and the `missing_injections` filter behavior. Framework (fw-4.16.2): - STRAYMARK.md §Directive Injection Markers rewritten to describe the post-fix behavior: init, update-framework, AND repair all walk manifest.injections and create any missing target. - Governance footers + version tables bumped. - Fix asymmetry: EN CLI-REFERENCE CLI row had been at cli-3.13.2 since the fw-4.15.0 cycle — corrected. All 530 tests pass. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c059be4 commit 6f46d30

27 files changed

Lines changed: 231 additions & 42 deletions

CHANGELOG.md

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

88
---
99

10+
## Framework 4.16.2 / CLI 3.14.1 — `straymark repair` per-target restore (Issue #156 follow-up)
11+
12+
Fixes the underlying CLI bug surfaced in the [Issue #156 closure comment](https://github.com/StrangeDaysTech/straymark/issues/156#issuecomment-4465050814): `straymark repair` ignored a single missing injection target (e.g. a deleted `AGENTS.md`) when `STRAYMARK.md` was still present, because both the download trigger and the inject path were gated on `STRAYMARK.md` being absent. From this release, both gates are removed and `repair` mirrors `straymark update-framework`'s per-target behavior for the set of files declared under `dist-manifest.yml::injections:`.
13+
14+
### Fixed (CLI)
15+
16+
- **`straymark repair` per-target restore** (`cli/src/commands/repair.rs`) — two gating bugs removed:
17+
- `check_needs_download()` now reads the local `dist-manifest.yml` and flags the download as needed if any declared injection target is missing. Before this change a deleted directive file (`AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `.github/copilot-instructions.md`, `.cursorrules`, `.cursor/rules/straymark.md`) was silently ignored because the function only looked at essential framework files.
18+
- `restore_missing_files()` replaces the `if !straymark_md.exists()` gate around the inject loop with a per-target filter (`missing_injections`). Each declared injection target is now checked individually; only the missing ones load their template from the ZIP and run through `inject::inject_directive`. Targets already present are left untouched — `repair` does not overwrite an installed file.
19+
- **`cli/src/commands/repair.rs::tests`** — five new unit tests cover the helper-level behavior: `check_needs_download` returns true for a missing injection target / false when all are present / still true for missing essential files (regression for the prior behavior); `missing_injections` filter identifies per-target gaps and is empty when all targets are present. End-to-end coverage of the download + extract path remains out of scope for the unit suite (it requires a real release ZIP and network); the release workflow exercises that path on every tag.
20+
21+
### Changed (Framework)
22+
23+
- **`dist/STRAYMARK.md §Directive Injection Markers`** — the missing-target behavior bullet added in `fw-4.16.1` is rewritten to describe the post-`cli-3.14.1` behavior: `init`, `update-framework`, AND `repair` all walk `manifest.injections` and create any missing target file. A short historical note preserves the pre-fix behavior for adopters running older CLIs.
24+
- **Governance footers** bumped to `v4.16.2` across `QUICK-REFERENCE.md`, `AGENT-RULES.md`, `DOCUMENTATION-POLICY.md`, `C4-DIAGRAM-GUIDE.md` (EN + ES + zh-CN) and the `CHARTER-CHAIN-EVOLUTION.md` footer (3 languages).
25+
- **Version tables** in `README.md` + i18n and `CLI-REFERENCE.md` + i18n bumped to `fw-4.16.2` / `cli-3.14.1`. The EN `CLI-REFERENCE.md` CLI row had been left at `cli-3.13.2` since the `fw-4.15.0` cycle — corrected in this patch.
26+
27+
### Adopter guidance
28+
29+
`straymark update-cli` brings `cli-3.14.1` with the fix. `straymark update-framework` brings the docs sync. Adopters who had been working around the bug by manually re-creating a deleted `AGENTS.md` before running `repair` no longer need that step — `straymark repair` will restore any single missing injection target without requiring the rest of the install to be broken.
30+
31+
---
32+
1033
## Framework 4.16.1 — docs polish (Issue #156 follow-up)
1134

1235
Documentation-only patch addressing two non-blocking observations raised in the [Issue #156 closure comment](https://github.com/StrangeDaysTech/straymark/issues/156#issuecomment-4465050814) after the adopter pulled `fw-4.16.0` / `cli-3.14.0`. No CLI bump.

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,8 @@ StrayMark uses independent version tags for each component:
276276

277277
| Component | Tag prefix | Example | Includes |
278278
| --- | --- | --- | --- |
279-
| Framework | `fw-` | `fw-4.16.1` | Templates (12 types), governance, directives, Charter template + schema |
280-
| CLI | `cli-` | `cli-3.14.0` | The `straymark` binary |
279+
| Framework | `fw-` | `fw-4.16.2` | Templates (12 types), governance, directives, Charter template + schema |
280+
| CLI | `cli-` | `cli-3.14.1` | The `straymark` binary |
281281

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

@@ -309,7 +309,7 @@ See [CLI Reference](https://github.com/StrangeDaysTech/straymark/blob/main/docs/
309309
```bash
310310
# Download the latest framework release ZIP from GitHub
311311
# Go to https://github.com/StrangeDaysTech/straymark/releases
312-
# and download the latest fw-* release (e.g., fw-4.16.1)
312+
# and download the latest fw-* release (e.g., fw-4.16.2)
313313
314314
# Extract and copy to your project
315315
unzip straymark-fw-*.zip -d your-project/

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.0"
3+
version = "3.14.1"
44
edition = "2021"
55
description = "CLI for StrayMark — the cognitive discipline your AI-assisted projects need"
66
license = "MIT"

cli/src/commands/repair.rs

Lines changed: 174 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::path::{Path, PathBuf};
66
use crate::config::Checksums;
77
use crate::download;
88
use crate::inject;
9-
use crate::manifest::DistManifest;
9+
use crate::manifest::{DistManifest, Injection};
1010
use crate::utils;
1111

1212
/// Expected directories inside .straymark/ (same as init.rs)
@@ -184,6 +184,21 @@ fn check_needs_download(target: &Path) -> bool {
184184
}
185185
}
186186

187+
// Check injection targets declared in the local manifest (fw-4.16.2 / cli-3.14.1).
188+
// Before this version, `repair` only triggered the framework download when one
189+
// of the essential framework files was missing — a single deleted injection
190+
// target (e.g. `AGENTS.md` while `STRAYMARK.md` remained intact) was a silent
191+
// no-op. Walking `manifest.injections` here brings the download phase in line
192+
// with the restore phase, which now checks each target individually.
193+
let manifest_path = target.join(".straymark/dist-manifest.yml");
194+
if let Ok(manifest) = DistManifest::load(&manifest_path) {
195+
for injection in &manifest.injections {
196+
if !target.join(&injection.target).exists() {
197+
return true;
198+
}
199+
}
200+
}
201+
187202
false
188203
}
189204

@@ -227,12 +242,26 @@ fn restore_missing_files(zip_path: &Path, target: &Path) -> Result<()> {
227242
restored += extract_missing_files(&mut archive, &prefix, pattern, target)?;
228243
}
229244

230-
// Restore injections if STRAYMARK.md or directive files are missing
231-
let straymark_md = target.join("STRAYMARK.md");
232-
if !straymark_md.exists() {
233-
// Read templates for injection
245+
// Restore each missing injection target individually (fw-4.16.2 / cli-3.14.1).
246+
// Before this version, this block was gated on `!STRAYMARK.md.exists()` — which
247+
// meant a single missing injection target (e.g. `AGENTS.md` while `STRAYMARK.md`
248+
// remained intact) was silently skipped, contradicting the `straymark repair`
249+
// promise to restore broken installations. The per-target presence check below
250+
// brings repair in line with the documented intent and with what `straymark
251+
// update-framework` already does for the same set of files.
252+
let missing_injections: Vec<&Injection> = manifest
253+
.injections
254+
.iter()
255+
.filter(|inj| !target.join(&inj.target).exists())
256+
.collect();
257+
258+
if !missing_injections.is_empty() {
259+
// Load only the templates we will actually re-inject.
234260
let mut templates: HashMap<String, String> = HashMap::new();
235-
for injection in &manifest.injections {
261+
for injection in &missing_injections {
262+
if templates.contains_key(&injection.template) {
263+
continue;
264+
}
236265
let zip_entry_name = format!("{}{}", prefix, injection.template);
237266
for i in 0..archive.len() {
238267
let mut entry = archive.by_index(i)?;
@@ -245,8 +274,8 @@ fn restore_missing_files(zip_path: &Path, target: &Path) -> Result<()> {
245274
}
246275
}
247276

248-
// Re-inject directives
249-
for injection in &manifest.injections {
277+
// Re-inject only the missing ones.
278+
for injection in &missing_injections {
250279
let template_content = match templates.get(&injection.template) {
251280
Some(content) => content,
252281
None => continue,
@@ -378,3 +407,140 @@ fn walkdir(dir: PathBuf) -> Result<Vec<PathBuf>> {
378407
}
379408
Ok(files)
380409
}
410+
411+
#[cfg(test)]
412+
mod tests {
413+
use super::*;
414+
use crate::manifest::Injection;
415+
use tempfile::TempDir;
416+
417+
fn write_manifest(target: &Path, injection_targets: &[&str]) {
418+
let manifest_path = target.join(".straymark/dist-manifest.yml");
419+
std::fs::create_dir_all(manifest_path.parent().unwrap()).unwrap();
420+
let injections_yaml: String = injection_targets
421+
.iter()
422+
.map(|t| {
423+
format!(
424+
" - target: {t}\n template: dist-templates/directives/dummy.md\n"
425+
)
426+
})
427+
.collect();
428+
let body = format!(
429+
"version: \"4.16.2\"\ndescription: \"test\"\nrepository: \"x\"\nfiles: []\ninjections:\n{injections_yaml}"
430+
);
431+
std::fs::write(&manifest_path, body).unwrap();
432+
}
433+
434+
fn write_essential_framework_files(target: &Path) {
435+
// Files `check_needs_download` requires to be present in order to NOT
436+
// already short-circuit to true for other reasons (we want to isolate
437+
// the "missing injection target" branch).
438+
std::fs::create_dir_all(target.join(".straymark/templates")).unwrap();
439+
std::fs::write(target.join(".straymark/templates/template.md"), "x").unwrap();
440+
std::fs::create_dir_all(target.join(".straymark/00-governance")).unwrap();
441+
std::fs::write(target.join(".straymark/00-governance/AGENT-RULES.md"), "x").unwrap();
442+
std::fs::write(target.join(".straymark/config.yml"), "language: en\n").unwrap();
443+
std::fs::write(target.join("STRAYMARK.md"), "# StrayMark\n").unwrap();
444+
}
445+
446+
#[test]
447+
fn check_needs_download_true_when_injection_target_missing() {
448+
let dir = TempDir::new().unwrap();
449+
write_essential_framework_files(dir.path());
450+
write_manifest(dir.path(), &["AGENTS.md", "CLAUDE.md"]);
451+
// Pre-create one target, leave the other missing.
452+
std::fs::write(dir.path().join("AGENTS.md"), "existing\n").unwrap();
453+
454+
assert!(
455+
check_needs_download(dir.path()),
456+
"must trigger download when an injection target listed in the manifest is missing"
457+
);
458+
}
459+
460+
#[test]
461+
fn check_needs_download_false_when_all_injection_targets_present() {
462+
let dir = TempDir::new().unwrap();
463+
write_essential_framework_files(dir.path());
464+
write_manifest(dir.path(), &["AGENTS.md", "CLAUDE.md"]);
465+
std::fs::write(dir.path().join("AGENTS.md"), "x").unwrap();
466+
std::fs::write(dir.path().join("CLAUDE.md"), "x").unwrap();
467+
468+
assert!(
469+
!check_needs_download(dir.path()),
470+
"must NOT trigger download when nothing is missing"
471+
);
472+
}
473+
474+
#[test]
475+
fn check_needs_download_true_when_essential_file_missing_independent_of_injections() {
476+
// Regression: pre-fw-4.16.2 behavior must still hold for the essential-file path.
477+
let dir = TempDir::new().unwrap();
478+
write_essential_framework_files(dir.path());
479+
write_manifest(dir.path(), &[]);
480+
std::fs::remove_file(dir.path().join("STRAYMARK.md")).unwrap();
481+
482+
assert!(
483+
check_needs_download(dir.path()),
484+
"missing STRAYMARK.md must still trigger a download"
485+
);
486+
}
487+
488+
#[test]
489+
fn missing_injections_filter_identifies_per_target_gaps() {
490+
// Exercises the same filter expression used in `restore_missing_files`
491+
// (and verifies it does not regress to the pre-fw-4.16.2 STRAYMARK.md gate).
492+
let dir = TempDir::new().unwrap();
493+
write_essential_framework_files(dir.path());
494+
std::fs::write(dir.path().join("AGENTS.md"), "x").unwrap();
495+
// CLAUDE.md intentionally missing.
496+
497+
let injections = vec![
498+
Injection {
499+
target: "AGENTS.md".to_string(),
500+
template: "dist-templates/directives/AGENTS.md".to_string(),
501+
embed: None,
502+
},
503+
Injection {
504+
target: "CLAUDE.md".to_string(),
505+
template: "dist-templates/directives/CLAUDE.md".to_string(),
506+
embed: None,
507+
},
508+
];
509+
510+
let missing: Vec<&Injection> = injections
511+
.iter()
512+
.filter(|inj| !dir.path().join(&inj.target).exists())
513+
.collect();
514+
515+
assert_eq!(missing.len(), 1, "exactly one target should be flagged missing");
516+
assert_eq!(missing[0].target, "CLAUDE.md");
517+
}
518+
519+
#[test]
520+
fn missing_injections_filter_is_empty_when_all_present() {
521+
let dir = TempDir::new().unwrap();
522+
write_essential_framework_files(dir.path());
523+
std::fs::write(dir.path().join("AGENTS.md"), "x").unwrap();
524+
std::fs::write(dir.path().join("CLAUDE.md"), "x").unwrap();
525+
526+
let injections = vec![
527+
Injection {
528+
target: "AGENTS.md".to_string(),
529+
template: "x".to_string(),
530+
embed: None,
531+
},
532+
Injection {
533+
target: "CLAUDE.md".to_string(),
534+
template: "x".to_string(),
535+
embed: None,
536+
},
537+
];
538+
539+
let missing: Vec<&Injection> = injections
540+
.iter()
541+
.filter(|inj| !dir.path().join(&inj.target).exists())
542+
.collect();
543+
544+
assert!(missing.is_empty());
545+
}
546+
}

dist/.straymark/00-governance/AGENT-RULES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,4 +379,4 @@ When a project accumulates a high volume of AILOGs across multiple Charters and
379379
380380
---
381381
382-
*StrayMark v4.16.1 | [Strange Days Tech](https://strangedays.tech)*
382+
*StrayMark v4.16.2 | [Strange Days Tech](https://strangedays.tech)*

dist/.straymark/00-governance/C4-DIAGRAM-GUIDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,4 +234,4 @@ Use a Level 1 (Context) diagram to illustrate:
234234

235235
---
236236

237-
*StrayMark v4.16.1 | [Strange Days Tech](https://strangedays.tech)*
237+
*StrayMark v4.16.2 | [Strange Days Tech](https://strangedays.tech)*

dist/.straymark/00-governance/CHARTER-CHAIN-EVOLUTION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,4 +193,4 @@ This document is itself the output of the acceptance flow Sentinel walked throug
193193

194194
---
195195

196-
*StrayMark fw-4.16.1 | [GitHub](https://github.com/StrangeDaysTech/straymark) | Issue [#156](https://github.com/StrangeDaysTech/straymark/issues/156)*
196+
*StrayMark fw-4.16.2 | [GitHub](https://github.com/StrangeDaysTech/straymark) | Issue [#156](https://github.com/StrangeDaysTech/straymark/issues/156)*

dist/.straymark/00-governance/DOCUMENTATION-POLICY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,4 +318,4 @@ See also [ADR-2025-01-20-001] for architectural context.
318318
319319
---
320320

321-
*StrayMark v4.16.1 | [Strange Days Tech](https://strangedays.tech)*
321+
*StrayMark v4.16.2 | [Strange Days Tech](https://strangedays.tech)*

dist/.straymark/00-governance/QUICK-REFERENCE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,4 +241,4 @@ Mark `review_required: true` when:
241241

242242
---
243243

244-
*StrayMark v4.16.1 | [Strange Days Tech](https://strangedays.tech)*
244+
*StrayMark v4.16.2 | [Strange Days Tech](https://strangedays.tech)*

0 commit comments

Comments
 (0)