@sdd-method/maintain-cli 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/dist/lib/agent-harness.js +2 -2
  2. package/dist/lib/briefing/index.js +1 -1
  3. package/dist/lib/briefing/render.js +1 -1
  4. package/dist/lib/briefing/variables.js +1 -1
  5. package/dist/lib/bundle.js +1 -1
  6. package/dist/lib/catalogue/aggregate.js +7 -7
  7. package/dist/lib/catalogue/application-archetype/vocabulary.js +2 -2
  8. package/dist/lib/catalogue/archetype/maps/android.yaml +2 -2
  9. package/dist/lib/catalogue/archetype/maps/swiftui.yaml +2 -2
  10. package/dist/lib/catalogue/archetype/maps/uikit.yaml +2 -2
  11. package/dist/lib/catalogue/archetype/maps/web-generic.yaml +2 -2
  12. package/dist/lib/catalogue/archetype/openui-archetype-vocabulary.yaml +3 -3
  13. package/dist/lib/catalogue/archetype/resolver.js +4 -4
  14. package/dist/lib/catalogue/archetype/wireframe.js +1 -1
  15. package/dist/lib/catalogue/build.js +33 -33
  16. package/dist/lib/catalogue/builders/adr-supersedes.js +3 -3
  17. package/dist/lib/catalogue/builders/adrs.js +3 -3
  18. package/dist/lib/catalogue/builders/c4-containers.js +2 -2
  19. package/dist/lib/catalogue/builders/c4-entity-rollups.js +1 -1
  20. package/dist/lib/catalogue/builders/c4-external-systems.js +2 -2
  21. package/dist/lib/catalogue/builders/c4-systems.js +1 -1
  22. package/dist/lib/catalogue/builders/capabilities.js +1 -1
  23. package/dist/lib/catalogue/builders/capability-domain-model.js +1 -1
  24. package/dist/lib/catalogue/builders/capability-spec-walker.js +2 -2
  25. package/dist/lib/catalogue/builders/capability-spec.js +4 -4
  26. package/dist/lib/catalogue/builders/contracts.js +3 -3
  27. package/dist/lib/catalogue/builders/data-store-mapping.js +2 -2
  28. package/dist/lib/catalogue/builders/domain-models.js +2 -2
  29. package/dist/lib/catalogue/builders/domains.js +3 -3
  30. package/dist/lib/catalogue/builders/entities-manifest.js +8 -8
  31. package/dist/lib/catalogue/builders/entities-mermaid.js +4 -4
  32. package/dist/lib/catalogue/builders/entity-persistence.js +1 -1
  33. package/dist/lib/catalogue/builders/entity-slug.js +3 -3
  34. package/dist/lib/catalogue/builders/event-catalog-walker.js +6 -6
  35. package/dist/lib/catalogue/builders/event-edges.js +6 -6
  36. package/dist/lib/catalogue/builders/events.js +3 -3
  37. package/dist/lib/catalogue/builders/evidence-class.js +1 -1
  38. package/dist/lib/catalogue/builders/feature-domain-model.js +1 -1
  39. package/dist/lib/catalogue/builders/feature-grounding-walker.js +2 -2
  40. package/dist/lib/catalogue/builders/feature-grounding.js +5 -5
  41. package/dist/lib/catalogue/builders/feature-subscription-channels.js +3 -3
  42. package/dist/lib/catalogue/builders/features.js +1 -1
  43. package/dist/lib/catalogue/builders/glossary-xref.js +4 -4
  44. package/dist/lib/catalogue/builders/integrations.js +4 -4
  45. package/dist/lib/catalogue/builders/persona-c4system.js +1 -1
  46. package/dist/lib/catalogue/builders/product-edges.js +1 -1
  47. package/dist/lib/catalogue/builders/product-platform-dependencies.js +2 -2
  48. package/dist/lib/catalogue/builders/products.js +5 -5
  49. package/dist/lib/catalogue/builders/repo-edges.js +12 -12
  50. package/dist/lib/catalogue/builders/repositories.js +11 -7
  51. package/dist/lib/catalogue/builders/screen-tree.js +6 -6
  52. package/dist/lib/catalogue/builders/screens.js +18 -18
  53. package/dist/lib/catalogue/builders/service-contract-consumes.js +1 -1
  54. package/dist/lib/catalogue/builders/service-entity-tables.js +3 -3
  55. package/dist/lib/catalogue/builders/services.js +5 -5
  56. package/dist/lib/catalogue/builders/system-external-dependency.js +2 -2
  57. package/dist/lib/catalogue/coverage-render.js +15 -15
  58. package/dist/lib/catalogue/csv-writer.js +1 -1
  59. package/dist/lib/catalogue/feature-grounding-aliases-loader.js +1 -1
  60. package/dist/lib/catalogue/findings.js +1 -1
  61. package/dist/lib/catalogue/fk-validator.js +2 -2
  62. package/dist/lib/catalogue/manifest-loader.js +3 -3
  63. package/dist/lib/catalogue/schema-compat.js +2 -2
  64. package/dist/lib/catalogue/schema-loader.js +1 -1
  65. package/dist/lib/catalogue/screen-validation-loader.js +2 -2
  66. package/dist/lib/catalogue/service-architecture-parser.js +1 -1
  67. package/dist/lib/catalogue/traceability-loader.js +6 -4
  68. package/dist/lib/generate-briefing/index.js +2 -2
  69. package/dist/lib/hooks-seed/entries.js +1 -1
  70. package/dist/lib/metamodel/tql-subset-parser.js +280 -0
  71. package/dist/lib/pack/builder.js +2 -2
  72. package/dist/lib/pack/index.js +1 -1
  73. package/dist/lib/pack/leakage.js +1 -1
  74. package/dist/lib/pack/publish-transform.js +2 -2
  75. package/dist/lib/pack/schema.js +2 -2
  76. package/dist/lib/pack/seed.js +2 -2
  77. package/dist/lib/pack/validate.js +1 -1
  78. package/dist/lib/repo-kind.js +3 -3
  79. package/dist/lib/scaffold/archetype-behaviour.js +171 -0
  80. package/dist/lib/scaffold/registry.js +136 -0
  81. package/dist/lib/sync/briefing-merge.js +2 -2
  82. package/dist/lib/sync/bundle-cache.js +4 -4
  83. package/dist/lib/sync/index.js +7 -7
  84. package/dist/lib/sync/settings-merge.js +1 -1
  85. package/dist/lib/sync/yaml-block-merge.js +1 -1
  86. package/dist/lib/validate/archetype-citations.js +134 -0
  87. package/dist/lib/validate/behaviour-store.js +25 -1
  88. package/dist/lib/validate/dispatch.js +8 -1
  89. package/dist/maintain-program.js +2 -2
  90. package/dist/maintain-version.json +2 -2
  91. package/dist/verbs/list.js +1 -1
  92. package/dist/verbs/pack-build.js +2 -2
  93. package/dist/verbs/render.js +1 -1
  94. package/dist/verify-program.js +2 -2
  95. package/dist/verify-version.json +2 -2
  96. package/package.json +2 -2
  97. package/scripts/validate-app-dependencies.sh +1 -1
  98. package/scripts/validate-integration-docs.sh +1 -1
@@ -0,0 +1,136 @@
1
+ import { readFile, readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { parse as parseYaml } from "yaml";
4
+ import { BUNDLE_CACHE_REL, REGISTRY_FILE } from "../sync/bundle-cache.js";
5
+ /**
6
+ * Scan the bundle cache for registries and return every archetype found.
7
+ * Bundles without any registry.yaml are skipped silently (vendor packs
8
+ * etc.). A registry may sit at the bundle's payload root or nested
9
+ * (multi-bundle producer repos keep one registry per bundle subtree,
10
+ * e.g. a vertical overlay's `verticals/fleet/registry.yaml`) — all are
11
+ * read; seed paths stay payload-root-relative regardless of where the
12
+ * declaring registry sits.
13
+ */
14
+ export async function discoverArchetypes(targetRepo) {
15
+ const cacheRoot = join(targetRepo, BUNDLE_CACHE_REL);
16
+ let entries;
17
+ try {
18
+ entries = await readdir(cacheRoot, { withFileTypes: true });
19
+ }
20
+ catch {
21
+ return [];
22
+ }
23
+ const out = [];
24
+ for (const e of entries) {
25
+ if (!e.isDirectory())
26
+ continue;
27
+ const bundleCacheDir = join(cacheRoot, e.name);
28
+ const at = e.name.lastIndexOf("@");
29
+ const bundleName = at > 0 ? e.name.slice(0, at) : e.name;
30
+ for (const registryPath of await findRegistryFiles(bundleCacheDir)) {
31
+ let raw;
32
+ try {
33
+ raw = await readFile(registryPath, "utf8");
34
+ }
35
+ catch {
36
+ continue;
37
+ }
38
+ let parsed;
39
+ try {
40
+ parsed = parseYaml(raw);
41
+ }
42
+ catch {
43
+ continue; // malformed registry — skip rather than break scaffold
44
+ }
45
+ if (parsed === null || typeof parsed !== "object")
46
+ continue;
47
+ if (!Array.isArray(parsed.archetypes))
48
+ continue;
49
+ const tier = typeof parsed.tier === "string" ? parsed.tier : "unknown";
50
+ for (const a of parsed.archetypes) {
51
+ if (a === null || typeof a !== "object")
52
+ continue;
53
+ const rec = a;
54
+ const id = typeof rec.id === "string" ? rec.id : undefined;
55
+ const seeds = Array.isArray(rec.seeds)
56
+ ? rec.seeds.filter((s) => typeof s === "string")
57
+ : [];
58
+ if (!id || seeds.length === 0)
59
+ continue;
60
+ out.push({
61
+ id,
62
+ tier,
63
+ seeds,
64
+ bundleName,
65
+ bundleCacheDir,
66
+ ...(typeof rec.layer === "string" && { layer: rec.layer }),
67
+ ...(typeof rec.domain_name === "string" && { domainName: rec.domain_name }),
68
+ ...(typeof rec.status === "string" && { status: rec.status }),
69
+ ...(typeof rec.notes === "string" && { notes: rec.notes }),
70
+ });
71
+ }
72
+ }
73
+ }
74
+ return out;
75
+ }
76
+ /** Depth cap for nested-registry discovery — bundle payloads are shallow. */
77
+ const REGISTRY_SCAN_MAX_DEPTH = 4;
78
+ /**
79
+ * Find every registry.yaml within a cached bundle (root + nested),
80
+ * depth-capped. Returns absolute paths in a stable (sorted) order.
81
+ */
82
+ async function findRegistryFiles(dir, depth = 0) {
83
+ let entries;
84
+ try {
85
+ entries = await readdir(dir, { withFileTypes: true });
86
+ }
87
+ catch {
88
+ return [];
89
+ }
90
+ const found = [];
91
+ for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
92
+ if (e.isFile() && e.name === REGISTRY_FILE) {
93
+ found.push(join(dir, e.name));
94
+ }
95
+ else if (e.isDirectory() && depth < REGISTRY_SCAN_MAX_DEPTH) {
96
+ found.push(...(await findRegistryFiles(join(dir, e.name), depth + 1)));
97
+ }
98
+ }
99
+ return found;
100
+ }
101
+ /**
102
+ * Resolve an archetype reference against the cache. Accepts bare ids
103
+ * (`core/identity`) and bundle-qualified ids
104
+ * (`ref-arch-core-b2b-saas:core/identity`).
105
+ */
106
+ export async function resolveArchetype(targetRepo, reference) {
107
+ const available = await discoverArchetypes(targetRepo);
108
+ let bundleFilter;
109
+ let id = reference;
110
+ const colon = reference.indexOf(":");
111
+ if (colon > 0) {
112
+ bundleFilter = reference.slice(0, colon);
113
+ id = reference.slice(colon + 1);
114
+ }
115
+ const matches = available.filter((a) => a.id === id && (bundleFilter === undefined || a.bundleName === bundleFilter));
116
+ if (matches.length === 1) {
117
+ return { ok: true, archetype: matches[0], available };
118
+ }
119
+ if (matches.length === 0) {
120
+ const known = available.map((a) => `${a.bundleName}:${a.id}`).join(", ");
121
+ return {
122
+ ok: false,
123
+ available,
124
+ error: available.length === 0
125
+ ? `No archetype registries found in ${BUNDLE_CACHE_REL}/ — sync an archetype-bearing bundle first (M-ADR 0176).`
126
+ : `Archetype '${reference}' not found in cached registries. Available: ${known}`,
127
+ };
128
+ }
129
+ const collisions = matches.map((a) => `${a.bundleName}:${a.id}`).join(", ");
130
+ return {
131
+ ok: false,
132
+ available,
133
+ error: `Archetype id '${id}' is declared by multiple bundles (${collisions}). Use the qualified form '<bundle>:<id>'.`,
134
+ };
135
+ }
136
+ //# sourceMappingURL=registry.js.map
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Briefing-layer sync per ADR 0148.
2
+ * Briefing-layer sync per M-ADR 0148.
3
3
  *
4
4
  * Two files participate, with different semantics:
5
5
  *
@@ -17,7 +17,7 @@
17
17
  *
18
18
  * Both files are rendered from templates that ship in the bundle at
19
19
  * `<target>/orchestration/templates/briefing/...`. Older bundles
20
- * pre-dating ADR 0148 won't carry the templates; this module skips
20
+ * pre-dating M-ADR 0148 won't carry the templates; this module skips
21
21
  * gracefully with a log line in that case.
22
22
  */
23
23
  import { access, mkdir, readFile, writeFile } from "node:fs/promises";
@@ -1,7 +1,7 @@
1
1
  import { appendFile, copyFile, mkdir, readFile, readdir, rm } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
  /**
4
- * Consumer-side bundle cache (ADR 0176 §4.2).
4
+ * Consumer-side bundle cache (M-ADR 0176 §4.2).
5
5
  *
6
6
  * Every consumed non-forward-base bundle is extracted to
7
7
  * `<targetRepo>/.sdd/bundle-cache/<bundleName>@<version>/` — the
@@ -15,7 +15,7 @@ import { dirname, join } from "node:path";
15
15
  */
16
16
  export const BUNDLE_CACHE_REL = ".sdd/bundle-cache";
17
17
  /**
18
- * The registry file name (ADR 0176 §4.1) — always cache-only. A bundle may
18
+ * The registry file name (M-ADR 0176 §4.1) — always cache-only. A bundle may
19
19
  * ship its registry at the payload root or nested (a multi-bundle producer
20
20
  * repo keeps one registry per bundle subtree, e.g. a vertical overlay's
21
21
  * `verticals/fleet/registry.yaml`, because the producer-layout rule forbids
@@ -33,7 +33,7 @@ export const REGISTRY_FILE = "registry.yaml";
33
33
  export const VOCAB_FILE = "application-archetypes.yaml";
34
34
  /**
35
35
  * Split a bundle's managed paths into the set to materialise into the
36
- * repo tree vs the set that stays cache-only (ADR 0176 §4.3):
36
+ * repo tree vs the set that stays cache-only (M-ADR 0176 §4.3):
37
37
  * scaffold-source-prefixed paths, registry files, and the archetype
38
38
  * vocabulary file are cache-only.
39
39
  */
@@ -103,7 +103,7 @@ export async function ensureCacheGitignored(targetRepo) {
103
103
  return false;
104
104
  }
105
105
  const prefixNewline = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
106
- await appendFile(giPath, `${prefixNewline}# sdd-cli bundle cache (ADR 0176) — extracted bundle payloads, not source\n${line}\n`);
106
+ await appendFile(giPath, `${prefixNewline}# sdd-cli bundle cache (M-ADR 0176) — extracted bundle payloads, not source\n${line}\n`);
107
107
  return true;
108
108
  }
109
109
  //# sourceMappingURL=bundle-cache.js.map
@@ -75,7 +75,7 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
75
75
  stdout(fail(`Missing payload file listed in MANAGED_PATHS.txt: ${missing[0]}`));
76
76
  return { exitCode: 1 };
77
77
  }
78
- // ADR 0176 §4.3: split consumption modes. Scaffold-source paths and
78
+ // M-ADR 0176 §4.3: split consumption modes. Scaffold-source paths and
79
79
  // the registry file extract to the bundle cache only; everything
80
80
  // else materialises into the repo tree as before.
81
81
  const { materialise: materialisePaths, cacheOnly: cacheOnlyPaths } = splitConsumptionModes(bundle.managedPaths, bundle.manifest.scaffoldSourcePaths);
@@ -155,7 +155,7 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
155
155
  }
156
156
  }
157
157
  // Additive bundles require a forward base already in the lock.
158
- // For adopter packs (ADR 0151) the requires_forward_base field
158
+ // For adopter packs (M-ADR 0151) the requires_forward_base field
159
159
  // declares which sdd-method bundle (and minimum version) is needed.
160
160
  const incomingBundleName = bundle.manifest.bundleName;
161
161
  const incomingIsForward = bundle.manifest.isForwardBase;
@@ -184,7 +184,7 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
184
184
  }
185
185
  }
186
186
  // Adopter pack requires_forward_base check: the named forward base
187
- // must be in the lock at a compatible version. Per ADR 0151 §4.2.
187
+ // must be in the lock at a compatible version. Per M-ADR 0151 §4.2.
188
188
  if (!incomingIsForward && bundle.manifest.requiresForwardBase) {
189
189
  const err = checkRequiresForwardBase(prevLock, bundle.manifest.requiresForwardBase, incomingBundleName);
190
190
  if (err !== null) {
@@ -193,7 +193,7 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
193
193
  }
194
194
  }
195
195
  // Adopter pack requires_pack check: vertical extensions name a base
196
- // pack they depend on. Per ADR 0151 §4.3.
196
+ // pack they depend on. Per M-ADR 0151 §4.3.
197
197
  if (!incomingIsForward && bundle.manifest.requiresPack) {
198
198
  const err = checkRequiresPack(prevLock, bundle.manifest.requiresPack, incomingBundleName);
199
199
  if (err !== null) {
@@ -451,7 +451,7 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
451
451
  for (const d of prunedDirs)
452
452
  stdout(` - ${d}/\n`);
453
453
  }
454
- // ADR 0176 §4.2: cache non-forward-base bundles (the resolution
454
+ // M-ADR 0176 §4.2: cache non-forward-base bundles (the resolution
455
455
  // substrate for scaffold --archetype). Forward bases are not cached
456
456
  // — they carry no archetype registries and are large.
457
457
  if (!bundle.manifest.isForwardBase) {
@@ -488,7 +488,7 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
488
488
  isForwardBase: incomingIsForward,
489
489
  managedPaths: [...bundle.managedPaths],
490
490
  pathDigests,
491
- // Adopter-pack provenance per ADR 0151 — fields only present for
491
+ // Adopter-pack provenance per M-ADR 0151 — fields only present for
492
492
  // non-forward-base bundles produced by adopter pack builders.
493
493
  ...(bundle.manifest.producedByAdopter && {
494
494
  producedByAdopter: bundle.manifest.producedByAdopter,
@@ -560,7 +560,7 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
560
560
  }
561
561
  for (const line of ci.log)
562
562
  stdout(info(line));
563
- // Briefing layer per ADR 0148: agent-instructions.md (unconditional
563
+ // Briefing layer per M-ADR 0148: agent-instructions.md (unconditional
564
564
  // overwrite) + harness wrapper (marker-aware merge). Templates ship
565
565
  // in the bundle's orchestration/templates/briefing/; gracefully skip
566
566
  // when an older bundle without them is being synced.
@@ -9,7 +9,7 @@
9
9
  * SDD_REPO_PATH value. An adopter's other servers, and an `sdd-mcp` entry that runs anything
10
10
  * else, are left alone.
11
11
  *
12
- * Conflict handling mirrors ADR 0114: if the adopter has modified a method-owned hook, sync
12
+ * Conflict handling mirrors M-ADR 0114: if the adopter has modified a method-owned hook, sync
13
13
  * refuses unless `--allow-overwrite` is passed.
14
14
  */
15
15
  import { access, mkdir, readFile, writeFile } from "node:fs/promises";
@@ -4,7 +4,7 @@
4
4
  * (including content above a lone trailing block, or below it, or
5
5
  * both) is preserved verbatim.
6
6
  *
7
- * Conflict model (per ADR 0114, narrowed to block scope):
7
+ * Conflict model (per M-ADR 0114, narrowed to block scope):
8
8
  * - If the existing file has no markers, treat it as adopter-authored.
9
9
  * Refuse to replace unless `allowOverwrite` is true.
10
10
  * - If the existing file has markers, upsert the block. Lines outside
@@ -0,0 +1,134 @@
1
+ // archetype-citations: an estate's citations of reference-architecture archetype behaviour still say what is true
2
+ // (seed once, then cite; scaffold/archetype-behaviour.ts writes them).
3
+ //
4
+ // For each docs/domains/<domain>/archetype-citations.yaml, each citation is resolved against the archetype's
5
+ // behaviour rows in the bundle the estate has cached (its locked version):
6
+ // unresolved the citations name no cached bundle or archetype, the cited requirement is gone from the
7
+ // archetype, the relation is not one of the four, a departure has no reason or decision, or the
8
+ // estate requirement it names exists in neither the living specs nor an open change;
9
+ // stale a derived, extended or narrowed citation whose archetype requirement no longer has the digest
10
+ // recorded: the archetype changed since the estate took or re-aligned it. Review it and re-pin
11
+ // (move `digest` and `version`), or record that the estate departs.
12
+ // A departed citation is never stale: departing is a decision, recorded with its reason, not drift. With no
13
+ // citations anywhere the check reports [N/A], an empty population, not a pass.
14
+ import { readdir, readFile } from "node:fs/promises";
15
+ import { join } from "node:path";
16
+ import { parse as parseYaml } from "yaml";
17
+ import { cachedVersion, CITATIONS_FILE, loadArchetypeBehaviour, RELATIONS } from "../scaffold/archetype-behaviour.js";
18
+ import { discoverArchetypes } from "../scaffold/registry.js";
19
+ const REQ_RE = /^###\s+Requirement:\s*(.+?)\s*$/gm;
20
+ async function list(dir) {
21
+ try {
22
+ return (await readdir(dir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
23
+ }
24
+ catch {
25
+ return [];
26
+ }
27
+ }
28
+ /** Every requirement title in the living specs and in open changes (a seeded change not yet archived counts). */
29
+ async function estateRequirements(repo) {
30
+ const titles = new Set();
31
+ const specFiles = [];
32
+ for (const d of await list(join(repo, "openspec", "specs")))
33
+ specFiles.push(join(repo, "openspec", "specs", d, "spec.md"));
34
+ for (const c of await list(join(repo, "openspec", "changes"))) {
35
+ if (c === "archive")
36
+ continue;
37
+ for (const d of await list(join(repo, "openspec", "changes", c, "specs"))) {
38
+ specFiles.push(join(repo, "openspec", "changes", c, "specs", d, "spec.md"));
39
+ }
40
+ }
41
+ for (const f of specFiles) {
42
+ let text;
43
+ try {
44
+ text = await readFile(f, "utf8");
45
+ }
46
+ catch {
47
+ continue;
48
+ }
49
+ for (const m of text.matchAll(REQ_RE))
50
+ titles.add(m[1]);
51
+ }
52
+ return titles;
53
+ }
54
+ export async function checkArchetypeCitations(repo) {
55
+ const domains = await list(join(repo, "docs", "domains"));
56
+ const files = [];
57
+ for (const d of domains) {
58
+ const p = join(repo, "docs", "domains", d, CITATIONS_FILE);
59
+ try {
60
+ files.push([`docs/domains/${d}/${CITATIONS_FILE}`, await readFile(p, "utf8")]);
61
+ }
62
+ catch {
63
+ // no citations for this domain
64
+ }
65
+ }
66
+ if (files.length === 0)
67
+ return { exitCode: 0, stdout: "[N/A] no archetype citations (docs/domains/*/archetype-citations.yaml) in this repository\n" };
68
+ const archetypes = await discoverArchetypes(repo);
69
+ const titles = await estateRequirements(repo);
70
+ const lines = [];
71
+ let problems = 0;
72
+ let total = 0;
73
+ const bad = (tag, msg) => {
74
+ lines.push(`[${tag}] ${msg}`);
75
+ problems++;
76
+ };
77
+ for (const [rel, text] of files) {
78
+ let doc;
79
+ try {
80
+ doc = (parseYaml(text) ?? {});
81
+ }
82
+ catch (e) {
83
+ bad("unresolved", `${rel}: does not parse: ${e.message.split("\n")[0]}`);
84
+ continue;
85
+ }
86
+ const archetype = archetypes.find((a) => a.id === doc.archetype && a.bundleName === doc.bundle);
87
+ if (!archetype) {
88
+ bad("unresolved", `${rel}: archetype ${String(doc.archetype)} of bundle ${String(doc.bundle)} is not in the bundle cache (sync the bundle)`);
89
+ continue;
90
+ }
91
+ const reqs = await loadArchetypeBehaviour(archetype.bundleCacheDir, archetype.seeds);
92
+ const byId = new Map((reqs ?? []).map((r) => [r.id, r]));
93
+ const at = cachedVersion(archetype.bundleCacheDir) ?? "the cached bundle";
94
+ const cites = Array.isArray(doc.citations) ? doc.citations : [];
95
+ if (cites.length === 0)
96
+ bad("unresolved", `${rel}: lists no citations`);
97
+ for (const c of cites) {
98
+ total++;
99
+ const tag = `${rel}: '${String(c.requirement)}' -> ${String(c.cites)}`;
100
+ const relation = String(c.relation);
101
+ if (!RELATIONS.includes(relation)) {
102
+ bad("unresolved", `${tag}: relation must be one of ${RELATIONS.join(", ")} (got ${relation})`);
103
+ continue;
104
+ }
105
+ if (relation === "departed" && (!c.reason || !c.decision)) {
106
+ bad("unresolved", `${tag}: a departure needs its reason and the decision that took it`);
107
+ continue;
108
+ }
109
+ if (relation !== "departed" && !titles.has(String(c.requirement))) {
110
+ bad("unresolved", `${tag}: this estate has no requirement titled so (living specs or open changes)`);
111
+ continue;
112
+ }
113
+ const src = byId.get(String(c.cites));
114
+ if (!src) {
115
+ bad("unresolved", `${tag}: ${archetype.bundleName} ${at} has no archetype requirement ${String(c.cites)}`);
116
+ continue;
117
+ }
118
+ if (relation === "departed") {
119
+ lines.push(`[ok] ${tag} (departed: ${String(c.reason)})`);
120
+ }
121
+ else if (src.digest !== String(c.digest)) {
122
+ bad("stale", `${tag}: the archetype requirement changed (${String(c.digest)} at ${String(doc.version)} -> ${src.digest} at ${at}); review it and re-pin, or record a departure`);
123
+ }
124
+ else {
125
+ lines.push(`[ok] ${tag} (${relation})`);
126
+ }
127
+ }
128
+ }
129
+ const verdict = problems
130
+ ? `[FAIL] archetype citations: ${problems} of ${total} unresolved or stale`
131
+ : `[PASS] archetype citations: ${total} resolve, none stale`;
132
+ return { exitCode: problems ? 1 : 0, stdout: lines.join("\n") + "\n" + verdict + "\n" };
133
+ }
134
+ //# sourceMappingURL=archetype-citations.js.map
@@ -20,6 +20,11 @@
20
20
  // never against itself (M-ADR 0222 §4.3, §4.4). A list the catalogue gives nothing to
21
21
  // check against (no Observed in line, or a repository with no catalogue contracts) fails:
22
22
  // a cross-check of zero contracts is unresolved, not a pass.
23
+ // 7. features: every living spec is keyed by a catalogue feature (M-ADR 0221 §4.7). A spec keyed by no
24
+ // feature is dropped from the catalogue by the requirements builder, which reports it only as a
25
+ // build finding; this check makes it a failure, so a shipped requirement cannot vanish from the
26
+ // catalogue with every check green. Without catalogues/features.csv the check is unresolved, not
27
+ // passed.
23
28
  // 6. scope: a baseline ADDS requirements; it REMOVES, MODIFIES or RENAMES only a requirement whose
24
29
  // latest archived change was a baseline, which is how a corrective baseline withdraws
25
30
  // what a baseline admitted wrongly. A requirement a judged change shipped is changed only
@@ -354,6 +359,25 @@ export async function checkBehaviourStore(repo) {
354
359
  lines.push(scopeChecked === 0 ? "scope: vacuous (no baseline removes, modifies or renames a requirement)" : `scope: ${scopeChecked} requirement(s) a baseline removes, modifies or renames, checked against their lineage`);
355
360
  for (const [a, b] of ties)
356
361
  lines.push(`order: ${a} and ${b} were archived the same day and the history does not order them; ordered by name`);
362
+ // 7. living specs keyed by a catalogue feature
363
+ const livingSpecs = await dirs(join(repo, STORE_DIR, "specs"));
364
+ if (livingSpecs.length > 0) {
365
+ const featuresCsv = await readText(join(repo, "catalogues", "features.csv"));
366
+ if (featuresCsv === null) {
367
+ failures.push(`${STORE_DIR}/specs: living specs cannot be checked against catalogue features: catalogues/features.csv is missing; build the catalogue`);
368
+ }
369
+ else {
370
+ const featureIds = new Set(parseCatalogueCsv(featuresCsv).rows.map((r) => r["id"] ?? ""));
371
+ const unkeyed = livingSpecs.filter((f) => !featureIds.has(f));
372
+ for (const f of unkeyed) {
373
+ failures.push(`${STORE_DIR}/specs/${f}: no catalogue feature has the id "${f}", so its requirements are not catalogued; give the behaviour a feature first (M-ADR 0222 §4.4)`);
374
+ }
375
+ lines.push(`features: ${livingSpecs.length} living spec(s), ${livingSpecs.length - unkeyed.length} keyed by a catalogue feature`);
376
+ }
377
+ }
378
+ else {
379
+ lines.push("features: vacuous (no living spec)");
380
+ }
357
381
  const population = recordFiles.length + open.length + archived.length;
358
382
  const body = lines.map((l) => ` ${l}`).join("\n") + "\n";
359
383
  if (failures.length > 0) {
@@ -361,7 +385,7 @@ export async function checkBehaviourStore(repo) {
361
385
  }
362
386
  if (population === 0)
363
387
  return { exitCode: 0, stdout: body + `[N/A] the behaviour store has no changes and no agreement records yet\n` };
364
- return { exitCode: 0, stdout: body + "[PASS] behaviour store: records, agreement, stale, verdict, surfaces and scope checks hold\n" };
388
+ return { exitCode: 0, stdout: body + "[PASS] behaviour store: records, agreement, stale, verdict, surfaces, scope and feature checks hold\n" };
365
389
  }
366
390
  /** Per repository, the contracts the catalogue says it serves (repo_contract ⋈ contracts.spec_path); null when either file is missing. */
367
391
  async function catalogueContracts(repo) {
@@ -1,3 +1,4 @@
1
+ import { checkArchetypeCitations } from "./archetype-citations.js";
1
2
  import { checkBehaviourStore, checkBehaviourStoreOpenspec } from "./behaviour-store.js";
2
3
  import { checkBundleCoherence } from "./bundle-coherence.js";
3
4
  import { checkYamlParse } from "./yaml-parse.js";
@@ -14,6 +15,12 @@ const BEHAVIOUR_STORE_VALIDATORS = [
14
15
  source: { kind: "native", run: async (ctx) => checkBehaviourStoreOpenspec(ctx.targetRepo) },
15
16
  build: () => ({ status: "run", args: [] }),
16
17
  },
18
+ // Seed once, then cite: requirements an estate took from a reference-architecture archetype still answer to it.
19
+ {
20
+ name: "archetype-citations",
21
+ source: { kind: "native", run: async (ctx) => checkArchetypeCitations(ctx.targetRepo) },
22
+ build: () => ({ status: "run", args: [] }),
23
+ },
17
24
  ];
18
25
  // Validators that accept --app-root / --repo-root flags work fine from the
19
26
  // bundled location because they override their internal ROOT_DIR via the
@@ -92,7 +99,7 @@ const APP_VALIDATORS = [
92
99
  ...BEHAVIOUR_STORE_VALIDATORS,
93
100
  // Note: validate-runway-classification is intentionally NOT in the app set.
94
101
  // It requires platform-level files (docs/platform/feature-tree.md,
95
- // architecture-runway-index.md) that app repos don't own. Per ADR 0089,
102
+ // architecture-runway-index.md) that app repos don't own. Per M-ADR 0089,
96
103
  // the reduced app suite is app-specific validators + shared foundations,
97
104
  // not platform validators.
98
105
  ];
@@ -80,14 +80,14 @@ export function buildMaintainProgram() {
80
80
  registerContext(program);
81
81
  registerRender(program);
82
82
  const catalogue = program.commands.find((c) => c.name() === "catalogue");
83
- catalogue.description("Build, aggregate, validate and query per-SDD catalogues (ADR 0134) — the derived model");
83
+ catalogue.description("Build, aggregate, validate and query per-SDD catalogues (M-ADR 0134) — the derived model");
84
84
  registerCatalogueBuild(catalogue);
85
85
  registerCatalogueCheck(catalogue);
86
86
  registerCatalogueAggregate(catalogue);
87
87
  registerCatalogueGraph(catalogue);
88
88
  registerCatalogueDiff(catalogue);
89
89
  const pack = program.commands.find((c) => c.name() === "pack");
90
- pack.description("Build and validate adopter-shipped context-pack bundles (ADR 0151)");
90
+ pack.description("Build and validate adopter-shipped context-pack bundles (M-ADR 0151)");
91
91
  registerPackBuild(pack);
92
92
  return program;
93
93
  }
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "0.8.0",
3
- "built_against_sdd_cli": "2.2.0"
2
+ "version": "0.9.0",
3
+ "built_against_sdd_cli": "2.3.0"
4
4
  }
@@ -7,7 +7,7 @@ const VALID_PROFILES = [
7
7
  export function registerList(program) {
8
8
  program
9
9
  .command("list")
10
- .description("Enumerate SDD repositories discoverable in the current workspace, including adopter pack provenance (ADR 0151)")
10
+ .description("Enumerate SDD repositories discoverable in the current workspace, including adopter pack provenance (M-ADR 0151)")
11
11
  .option("--profile <profile>", "filter by profile: platform | application | integration")
12
12
  .option("--workspace-root <path>", "workspace directory to walk (repeatable; defaults to cwd)", collectRoots, [])
13
13
  .option("--format <format>", "output format: text | json", "text")
@@ -11,8 +11,8 @@ export function registerPackBuild(pack) {
11
11
  .option("--producer-repo <dir>", "adopter repo root (managed_paths are resolved relative to this; defaults to cwd)", process.cwd())
12
12
  .option("--out-dir <dir>", "output directory for the tarball (defaults to <producer-repo>/dist/<pack-name>/)", resolvePathOption)
13
13
  .option("--forward-base-managed-paths <path>", "path to forward-base MANAGED_PATHS.txt for the leakage check (defaults to extracting from method-baseline.lock)")
14
- .option("--publish-transform", "apply the point-in-time / producer-agnostic publish transform (ADR 0183/0184): drop non-Accepted ADRs, strip lineage + stripped-class fields, flatten cross-repo refs, fail closed on leaks. For producer-published bundles.")
15
- .option("--origin-prefix", "with the publish transform, apply the range-aware origin prefix (ADR 0140 §4.1): 0NNN→M-ADR, 9NNN→RA-ADR, adopter 1NNN stays ADR; ADR files renamed to match. Implies --publish-transform. The RA-ADR bundle cutover switch.")
14
+ .option("--publish-transform", "apply the point-in-time / producer-agnostic publish transform (M-ADR 0183/0184): drop non-Accepted ADRs, strip lineage + stripped-class fields, flatten cross-repo refs, fail closed on leaks. For producer-published bundles.")
15
+ .option("--origin-prefix", "with the publish transform, apply the range-aware origin prefix (M-ADR 0140 §4.1): 0NNN→M-ADR, 9NNN→RA-ADR, adopter 1NNN stays ADR; ADR files renamed to match. Implies --publish-transform. The RA-ADR bundle cutover switch.")
16
16
  .option("--source-commit <sha>", "provenance from a release record: the commit the manifest records (with --source-branch, --source-repo and --generated-at)")
17
17
  .option("--source-branch <name>", "provenance from a release record: the branch the manifest records")
18
18
  .option("--source-repo <name>", "provenance from a release record: the repository name the manifest records, whatever the checkout is called")
@@ -10,7 +10,7 @@ export function registerRender(program) {
10
10
  .description("Render method artefacts to visual form");
11
11
  render
12
12
  .command("screen <file>")
13
- .description("Render a screen-spec's openui-lang block to a structural wireframe SVG (ADR 0169). " +
13
+ .description("Render a screen-spec's openui-lang block to a structural wireframe SVG (M-ADR 0169). " +
14
14
  "Platform comes from the `platform:` frontmatter; an adopter overlay is read from " +
15
15
  ".sdd/screen-validation.yaml when present.")
16
16
  .option("--out <file>", "write the SVG to a file (default: stdout)")
@@ -109,11 +109,11 @@ export function registerVerifySurface(program) {
109
109
  registerList(program);
110
110
  const pack = program
111
111
  .command("pack")
112
- .description("Validate adopter-shipped context-pack seeds (ADR 0151) — read-only");
112
+ .description("Validate adopter-shipped context-pack seeds (M-ADR 0151) — read-only");
113
113
  registerPackValidate(pack);
114
114
  const catalogue = program
115
115
  .command("catalogue")
116
- .description("Validate and query per-SDD catalogues (ADR 0134) — read-only, never rebuilds");
116
+ .description("Validate and query per-SDD catalogues (M-ADR 0134) — read-only, never rebuilds");
117
117
  registerCatalogueValidate(catalogue);
118
118
  registerCatalogueImpact(catalogue);
119
119
  registerCatalogueBaselineCheck(catalogue);
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "2.2.0",
3
- "built_against_sdd_cli": "2.2.0"
2
+ "version": "2.3.0",
3
+ "built_against_sdd_cli": "2.3.0"
4
4
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sdd-method/maintain-cli",
3
- "version": "0.8.0",
4
- "sddCliVersion": "2.2.0",
3
+ "version": "0.9.0",
4
+ "sddCliVersion": "2.3.0",
5
5
  "description": "Bring an SDD repository current and rebuild its derived model — sync, briefing, catalogue build and aggregate, pack build, context pull — plus every read-only verify verb. Ships only the code those verbs reach; it cannot author specification content. Authoring is in @sdd-method/sdd-cli.",
6
6
  "license": "UNLICENSED",
7
7
  "type": "module",
@@ -149,7 +149,7 @@ fi
149
149
  APP_DEPS_FILE="$APP_ROOT/app-platform-dependencies.yaml"
150
150
 
151
151
  # TWO AUTHORING SOURCES, NOT ONE.
152
- # The catalogue builder does multi-source ingestion under the ADR 0157 parity
152
+ # The catalogue builder does multi-source ingestion under the M-ADR 0157 parity
153
153
  # principle: `app-platform-dependencies.yaml` AND every
154
154
  # `reverse-evidence/**/feature-grounding-evidence.yaml`'s platform_dependencies
155
155
  # block, merged by primary key with forward winning. They are equal-status
@@ -160,7 +160,7 @@ check_manifest() {
160
160
  ;;
161
161
  esac
162
162
 
163
- # Interaction patterns (ADR 0171) — optional fields, validated only when present.
163
+ # Interaction patterns (M-ADR 0171) — optional fields, validated only when present.
164
164
  # Existing manifests predate the field; absence is not a failure. Directionality
165
165
  # mismatch (pattern present but the matching capability flag is false) is a warning.
166
166
  local ingestion invocation