@sdd-method/maintain-cli 0.7.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.
- package/dist/lib/agent-harness.js +2 -2
- package/dist/lib/briefing/index.js +1 -1
- package/dist/lib/briefing/render.js +1 -1
- package/dist/lib/briefing/variables.js +1 -1
- package/dist/lib/bundle.js +1 -1
- package/dist/lib/catalogue/aggregate.js +7 -7
- package/dist/lib/catalogue/application-archetype/vocabulary.js +2 -2
- package/dist/lib/catalogue/archetype/maps/android.yaml +2 -2
- package/dist/lib/catalogue/archetype/maps/swiftui.yaml +2 -2
- package/dist/lib/catalogue/archetype/maps/uikit.yaml +2 -2
- package/dist/lib/catalogue/archetype/maps/web-generic.yaml +2 -2
- package/dist/lib/catalogue/archetype/openui-archetype-vocabulary.yaml +3 -3
- package/dist/lib/catalogue/archetype/resolver.js +4 -4
- package/dist/lib/catalogue/archetype/wireframe.js +1 -1
- package/dist/lib/catalogue/build.js +33 -33
- package/dist/lib/catalogue/builders/adr-supersedes.js +3 -3
- package/dist/lib/catalogue/builders/adrs.js +3 -3
- package/dist/lib/catalogue/builders/c4-containers.js +2 -2
- package/dist/lib/catalogue/builders/c4-entity-rollups.js +1 -1
- package/dist/lib/catalogue/builders/c4-external-systems.js +2 -2
- package/dist/lib/catalogue/builders/c4-systems.js +1 -1
- package/dist/lib/catalogue/builders/capabilities.js +1 -1
- package/dist/lib/catalogue/builders/capability-domain-model.js +1 -1
- package/dist/lib/catalogue/builders/capability-spec-walker.js +2 -2
- package/dist/lib/catalogue/builders/capability-spec.js +4 -4
- package/dist/lib/catalogue/builders/contracts.js +12 -6
- package/dist/lib/catalogue/builders/data-store-mapping.js +2 -2
- package/dist/lib/catalogue/builders/domain-models.js +2 -2
- package/dist/lib/catalogue/builders/domains.js +3 -3
- package/dist/lib/catalogue/builders/entities-manifest.js +8 -8
- package/dist/lib/catalogue/builders/entities-mermaid.js +4 -4
- package/dist/lib/catalogue/builders/entity-persistence.js +1 -1
- package/dist/lib/catalogue/builders/entity-slug.js +3 -3
- package/dist/lib/catalogue/builders/event-catalog-walker.js +6 -6
- package/dist/lib/catalogue/builders/event-edges.js +6 -6
- package/dist/lib/catalogue/builders/events.js +3 -3
- package/dist/lib/catalogue/builders/evidence-class.js +1 -1
- package/dist/lib/catalogue/builders/feature-domain-model.js +1 -1
- package/dist/lib/catalogue/builders/feature-grounding-walker.js +2 -2
- package/dist/lib/catalogue/builders/feature-grounding.js +5 -5
- package/dist/lib/catalogue/builders/feature-subscription-channels.js +3 -3
- package/dist/lib/catalogue/builders/features.js +1 -1
- package/dist/lib/catalogue/builders/glossary-xref.js +4 -4
- package/dist/lib/catalogue/builders/integrations.js +4 -4
- package/dist/lib/catalogue/builders/persona-c4system.js +1 -1
- package/dist/lib/catalogue/builders/product-edges.js +1 -1
- package/dist/lib/catalogue/builders/product-platform-dependencies.js +2 -2
- package/dist/lib/catalogue/builders/products.js +5 -5
- package/dist/lib/catalogue/builders/repo-edges.js +12 -12
- package/dist/lib/catalogue/builders/repositories.js +16 -11
- package/dist/lib/catalogue/builders/screen-tree.js +6 -6
- package/dist/lib/catalogue/builders/screens.js +18 -18
- package/dist/lib/catalogue/builders/service-contract-consumes.js +1 -1
- package/dist/lib/catalogue/builders/service-entity-tables.js +3 -3
- package/dist/lib/catalogue/builders/services.js +5 -5
- package/dist/lib/catalogue/builders/system-external-dependency.js +2 -2
- package/dist/lib/catalogue/canonical-schema.yaml +3 -2
- package/dist/lib/catalogue/contract-id.js +1 -1
- package/dist/lib/catalogue/coverage-render.js +15 -15
- package/dist/lib/catalogue/csv-writer.js +1 -1
- package/dist/lib/catalogue/feature-grounding-aliases-loader.js +1 -1
- package/dist/lib/catalogue/findings.js +1 -1
- package/dist/lib/catalogue/fk-validator.js +2 -2
- package/dist/lib/catalogue/manifest-loader.js +3 -3
- package/dist/lib/catalogue/schema-compat.js +2 -2
- package/dist/lib/catalogue/schema-loader.js +1 -1
- package/dist/lib/catalogue/screen-validation-loader.js +2 -2
- package/dist/lib/catalogue/service-architecture-parser.js +1 -1
- package/dist/lib/catalogue/traceability-loader.js +9 -4
- package/dist/lib/generate-briefing/index.js +2 -2
- package/dist/lib/hooks-seed/entries.js +1 -1
- package/dist/lib/metamodel/tql-subset-parser.js +280 -0
- package/dist/lib/pack/builder.js +2 -2
- package/dist/lib/pack/index.js +1 -1
- package/dist/lib/pack/leakage.js +1 -1
- package/dist/lib/pack/publish-transform.js +2 -2
- package/dist/lib/pack/schema.js +2 -2
- package/dist/lib/pack/seed.js +2 -2
- package/dist/lib/pack/validate.js +1 -1
- package/dist/lib/repo-kind.js +3 -3
- package/dist/lib/scaffold/archetype-behaviour.js +171 -0
- package/dist/lib/scaffold/registry.js +136 -0
- package/dist/lib/sync/briefing-merge.js +2 -2
- package/dist/lib/sync/bundle-cache.js +4 -4
- package/dist/lib/sync/index.js +7 -7
- package/dist/lib/sync/settings-merge.js +1 -1
- package/dist/lib/sync/yaml-block-merge.js +1 -1
- package/dist/lib/validate/archetype-citations.js +134 -0
- package/dist/lib/validate/behaviour-store.js +25 -1
- package/dist/lib/validate/dispatch.js +8 -1
- package/dist/maintain-program.js +2 -2
- package/dist/maintain-version.json +2 -2
- package/dist/verbs/list.js +1 -1
- package/dist/verbs/pack-build.js +2 -2
- package/dist/verbs/render.js +1 -1
- package/dist/verify-program.js +2 -2
- package/dist/verify-version.json +2 -2
- package/package.json +2 -2
- package/scripts/validate-app-dependencies.sh +1 -1
- package/scripts/validate-integration-docs.sh +1 -1
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Archetype behaviour, seeded once and then cited (reference architecture sdm-d93u.4.1).
|
|
2
|
+
//
|
|
3
|
+
// A reference-architecture archetype ships its behaviour as canonical rows (behaviour.tql, rows of the
|
|
4
|
+
// behaviour schema in the M-ADR 0210 subset). An estate does not sync them: it takes them ONCE, when it
|
|
5
|
+
// scaffolds the domain, as an OpenSpec change that ADDS the archetype's requirements to its behaviour store,
|
|
6
|
+
// to agree and build through its own process like any other change (M-ADR 0221-0223). From then on the
|
|
7
|
+
// estate owns its requirements and CITES the archetype's: docs/domains/<domain>/archetype-citations.yaml
|
|
8
|
+
// records, per requirement, the archetype requirement it answers to, the bundle version and a digest of that
|
|
9
|
+
// requirement there, and how the estate's relates to it (derived, extended, narrowed, or departed with a
|
|
10
|
+
// reason and the decision that took it). The validator `archetype-citations` reports a citation whose source
|
|
11
|
+
// has changed since, so evolution is visible rather than silent; re-aligning is a re-pin.
|
|
12
|
+
//
|
|
13
|
+
// The generated OpenSpec spec the reference architecture keeps beside its rows is never read here: the rows
|
|
14
|
+
// are the source, and two homes for one behaviour is what M-ADR 0221 rules out.
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
16
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { stringify as stringifyYaml } from "yaml";
|
|
19
|
+
import { parseMeta } from "../metamodel/tql-subset-parser.js";
|
|
20
|
+
export const BEHAVIOUR_FILE = "behaviour.tql";
|
|
21
|
+
export const CITATIONS_FILE = "archetype-citations.yaml";
|
|
22
|
+
export const RELATIONS = ["derived", "extended", "narrowed", "departed"];
|
|
23
|
+
const BEHAVIOUR_META = new Set(["requirement-meta", "scenario-meta", "scenario-step-meta", "cite-meta"]);
|
|
24
|
+
const str = (r, k) => String(r[k] ?? "");
|
|
25
|
+
const num = (r, k) => Number(r[k] ?? 0);
|
|
26
|
+
/** The archetype's requirements, in position order, from its behaviour rows. */
|
|
27
|
+
export function parseArchetypeBehaviour(text, label) {
|
|
28
|
+
const rows = parseMeta(text, label, { metaTypes: BEHAVIOUR_META });
|
|
29
|
+
const reqs = [...(rows.get("requirement-meta") ?? [])].sort((a, b) => num(a, "m-position") - num(b, "m-position"));
|
|
30
|
+
const scen = rows.get("scenario-meta") ?? [];
|
|
31
|
+
const steps = rows.get("scenario-step-meta") ?? [];
|
|
32
|
+
const cites = rows.get("cite-meta") ?? [];
|
|
33
|
+
return reqs.map((r) => {
|
|
34
|
+
const id = str(r, "m-requirement");
|
|
35
|
+
const scenarios = scen
|
|
36
|
+
.filter((x) => str(x, "m-requirement") === id)
|
|
37
|
+
.sort((a, b) => num(a, "m-position") - num(b, "m-position"))
|
|
38
|
+
.map((x) => {
|
|
39
|
+
const sid = str(x, "m-scenario");
|
|
40
|
+
return {
|
|
41
|
+
id: sid,
|
|
42
|
+
title: str(x, "m-title"),
|
|
43
|
+
steps: steps
|
|
44
|
+
.filter((t) => str(t, "m-scenario") === sid)
|
|
45
|
+
.sort((a, b) => num(a, "m-position") - num(b, "m-position"))
|
|
46
|
+
.map((t) => ({ keyword: str(t, "m-keyword"), text: str(t, "m-text") })),
|
|
47
|
+
cites: cites.filter((c) => str(c, "m-scenario") === sid).map((c) => str(c, "m-cites")),
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
const req = {
|
|
51
|
+
id,
|
|
52
|
+
title: str(r, "m-title"),
|
|
53
|
+
statement: str(r, "m-statement"),
|
|
54
|
+
tier: str(r, "m-tier"),
|
|
55
|
+
ruleFile: str(r, "m-rule-file"),
|
|
56
|
+
ruleLabel: str(r, "m-rule-label"),
|
|
57
|
+
scenarios,
|
|
58
|
+
};
|
|
59
|
+
return { ...req, digest: requirementDigest(req) };
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** Digest of everything a citation answers to: wording, tier, source rule and scenarios, not position. */
|
|
63
|
+
export function requirementDigest(r) {
|
|
64
|
+
const canon = JSON.stringify([r.id, r.title, r.statement, r.tier, r.ruleFile, r.ruleLabel,
|
|
65
|
+
r.scenarios.map((s) => [s.id, s.title, s.steps.map((t) => [t.keyword, t.text]), [...s.cites].sort()])]);
|
|
66
|
+
return createHash("sha256").update(canon).digest("hex").slice(0, 12);
|
|
67
|
+
}
|
|
68
|
+
/** The archetype's behaviour rows in a cached bundle: <bundle cache>/<seed prefix>/behaviour.tql, if it ships them. */
|
|
69
|
+
export async function loadArchetypeBehaviour(bundleCacheDir, seeds) {
|
|
70
|
+
for (const seed of seeds) {
|
|
71
|
+
const file = join(bundleCacheDir, seed, BEHAVIOUR_FILE);
|
|
72
|
+
let text;
|
|
73
|
+
try {
|
|
74
|
+
text = await readFile(file, "utf8");
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
return parseArchetypeBehaviour(text, file);
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
/** `<bundle>@<version>` cache directory names carry the version the estate locked. */
|
|
84
|
+
export function cachedVersion(bundleCacheDir) {
|
|
85
|
+
const name = bundleCacheDir.replace(/[\\/]+$/, "").split(/[\\/]/).pop() ?? "";
|
|
86
|
+
const at = name.lastIndexOf("@");
|
|
87
|
+
return at > 0 ? name.slice(at + 1) : undefined;
|
|
88
|
+
}
|
|
89
|
+
function renderChange(domain, archetypeId, bundle, version, reqs) {
|
|
90
|
+
const proposal = [
|
|
91
|
+
"## Why", "",
|
|
92
|
+
`The ${domain} domain was scaffolded from the reference architecture's ${archetypeId} archetype (${bundle} ${version}). ` +
|
|
93
|
+
"The archetype prescribes behaviour every conforming estate needs; this change adds it to the behaviour store once. " +
|
|
94
|
+
"From here the requirements are this estate's: the citations in " +
|
|
95
|
+
`docs/domains/${domain}/${CITATIONS_FILE} record which archetype requirement each one answers to.`, "",
|
|
96
|
+
"## What Changes", "",
|
|
97
|
+
`- Adds ${reqs.length} requirements to the ${domain} spec, one per archetype rule, each with its scenarios.`,
|
|
98
|
+
"- Before agreeing the change, narrow, extend or remove requirements to fit this estate: an optional requirement whose condition " +
|
|
99
|
+
"the estate does not declare is removed; one the estate meets differently is reworded. Record each such choice in the citations " +
|
|
100
|
+
"(narrowed, extended, or departed with its reason and the decision that took it).",
|
|
101
|
+
"",
|
|
102
|
+
].join("\n");
|
|
103
|
+
const spec = ["## ADDED Requirements"];
|
|
104
|
+
for (const r of reqs) {
|
|
105
|
+
spec.push("", `### Requirement: ${r.title}`, "", r.statement, "", `Seeded from archetype requirement ${r.id} (${archetypeId}, ${r.tier}), rule "${r.ruleLabel}".`, "");
|
|
106
|
+
for (const s of r.scenarios) {
|
|
107
|
+
spec.push(`#### Scenario: ${s.title}`, "", ...s.steps.map((t) => `- **${t.keyword}** ${t.text}`), "");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const tasks = ["## 1. Fit", "", "- [ ] 1.1 Review each seeded requirement against this estate and record the relation in the citations", ""].join("\n");
|
|
111
|
+
return { proposal, spec: spec.join("\n").replace(/\n+$/, "") + "\n", tasks };
|
|
112
|
+
}
|
|
113
|
+
async function exists(p) {
|
|
114
|
+
try {
|
|
115
|
+
await access(p);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function writeAt(path, content) {
|
|
123
|
+
await mkdir(dirname(path), { recursive: true });
|
|
124
|
+
await writeFile(path, content, "utf8");
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Seed an estate's behaviour from an archetype, once: an OpenSpec change adding the archetype's requirements,
|
|
128
|
+
* and the domain's citations. Writes nothing when the estate has no behaviour store, when the archetype ships
|
|
129
|
+
* no rows, or when either the change or the citations already exist.
|
|
130
|
+
*/
|
|
131
|
+
export async function seedArchetypeBehaviour(input) {
|
|
132
|
+
const store = join(input.targetRepo, "openspec");
|
|
133
|
+
if (!(await exists(store)))
|
|
134
|
+
return { filesWritten: [], skipped: "no behaviour store (openspec/) in this repository" };
|
|
135
|
+
const reqs = await loadArchetypeBehaviour(input.bundleCacheDir, input.seeds);
|
|
136
|
+
if (!reqs || reqs.length === 0)
|
|
137
|
+
return { filesWritten: [], skipped: `archetype ${input.archetypeId} ships no behaviour rows` };
|
|
138
|
+
const version = cachedVersion(input.bundleCacheDir) ?? "unknown";
|
|
139
|
+
const change = `seed-${input.domain}-from-archetype`;
|
|
140
|
+
const changeDir = join(store, "changes", change);
|
|
141
|
+
const citations = join(input.targetRepo, "docs", "domains", input.domain, CITATIONS_FILE);
|
|
142
|
+
if (await exists(changeDir))
|
|
143
|
+
return { filesWritten: [], skipped: `openspec/changes/${change} already exists (seeded once)` };
|
|
144
|
+
if (await exists(citations))
|
|
145
|
+
return { filesWritten: [], skipped: `docs/domains/${input.domain}/${CITATIONS_FILE} already exists (seeded once)` };
|
|
146
|
+
const { proposal, spec, tasks } = renderChange(input.domain, input.archetypeId, input.bundleName, version, reqs);
|
|
147
|
+
const doc = {
|
|
148
|
+
archetype: input.archetypeId,
|
|
149
|
+
bundle: input.bundleName,
|
|
150
|
+
version,
|
|
151
|
+
citations: reqs.map((r) => ({ requirement: r.title, cites: r.id, digest: r.digest, relation: "derived" })),
|
|
152
|
+
};
|
|
153
|
+
const header = [
|
|
154
|
+
`# ${CITATIONS_FILE} — the ${input.domain} domain's requirements that answer to reference-architecture archetype requirements.`,
|
|
155
|
+
"# Seeded once from the archetype; the requirements are this estate's. Per citation: `requirement` is this estate's",
|
|
156
|
+
"# requirement title, `cites` the archetype requirement id, `digest` that requirement at `version`, and `relation` one of",
|
|
157
|
+
"# derived | extended | narrowed | departed (departed needs `reason` and `decision`). Checked by",
|
|
158
|
+
"# `sdd-cli validate --validator archetype-citations`: a changed source is reported until reviewed and re-pinned.",
|
|
159
|
+
"",
|
|
160
|
+
].join("\n");
|
|
161
|
+
const files = [
|
|
162
|
+
[join(changeDir, "proposal.md"), proposal],
|
|
163
|
+
[join(changeDir, "tasks.md"), tasks],
|
|
164
|
+
[join(changeDir, "specs", input.domain, "spec.md"), spec],
|
|
165
|
+
[citations, header + stringifyYaml(doc, { lineWidth: 0 })],
|
|
166
|
+
];
|
|
167
|
+
for (const [p, c] of files)
|
|
168
|
+
await writeAt(p, c);
|
|
169
|
+
return { filesWritten: files.map(([p]) => p.slice(input.targetRepo.length + 1)) };
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=archetype-behaviour.js.map
|
|
@@ -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
|
package/dist/lib/sync/index.js
CHANGED
|
@@ -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
|
|
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
|
];
|