@sdd-method/author-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/author-program.js +2 -1
- package/dist/lib/agent-harness.js +2 -2
- package/dist/lib/behaviour-store/archive.js +28 -1
- package/dist/lib/behaviour-store/schema.js +85 -0
- 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/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 +36 -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 +3 -3
- 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 +11 -7
- package/dist/lib/catalogue/builders/requirements.js +110 -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 +138 -2
- 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/rubric-coverage-map.yaml +28 -0
- 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 +6 -4
- package/dist/lib/conformance/finding.js +1 -1
- package/dist/lib/conformance/index.js +5 -5
- package/dist/lib/generate-briefing/index.js +2 -2
- package/dist/lib/metamodel/tql-subset-parser.js +280 -0
- 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/plan/read-sdd.js +1 -1
- package/dist/lib/repo-kind.js +4 -12
- package/dist/lib/scaffold/archetype-behaviour.js +173 -0
- package/dist/lib/scaffold/behaviour-store.js +60 -0
- package/dist/lib/scaffold/domain.js +24 -6
- package/dist/lib/scaffold/registry.js +1 -1
- package/dist/lib/sync/briefing-merge.js +2 -2
- package/dist/lib/sync/bundle-cache.js +4 -4
- package/dist/lib/sync/yaml-block-merge.js +1 -1
- package/dist/lib/validate/archetype-citations.js +169 -0
- package/dist/lib/validate/behaviour-store.js +161 -6
- package/dist/lib/validate/dispatch.js +8 -1
- package/dist/maintain-version.json +2 -2
- package/dist/verbs/conformance.js +3 -3
- package/dist/verbs/list.js +1 -1
- package/dist/verbs/render.js +1 -1
- package/dist/verbs/scaffold.js +36 -4
- 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
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { access, copyFile, mkdir, readdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { BEHAVIOUR_FILE, seedArchetypeBehaviour } from "./archetype-behaviour.js";
|
|
2
3
|
import { dirname, join, relative } from "node:path";
|
|
3
4
|
/**
|
|
4
|
-
* Domain scaffolding (ADR 0173). Writes the invariant domain shape into a
|
|
5
|
+
* Domain scaffolding (M-ADR 0173). Writes the invariant domain shape into a
|
|
5
6
|
* platform-root SDD, optionally seeded from a domain archetype. The generic
|
|
6
7
|
* shape is method-owned and domain-agnostic; archetypes overlay starter content.
|
|
7
8
|
*
|
|
8
9
|
* Only the `integration` archetype is built in (the method tier — the method
|
|
9
|
-
* owns that vocabulary via ADR 0171/0172). Reference-architecture and adopter
|
|
10
|
+
* owns that vocabulary via M-ADR 0171/0172). Reference-architecture and adopter
|
|
10
11
|
* archetypes are resolved from a consumed registry (not in this prototype).
|
|
11
12
|
*/
|
|
12
13
|
export const METHOD_DOMAIN_ARCHETYPES = ["integration"];
|
|
@@ -25,7 +26,7 @@ export async function scaffoldDomain(input) {
|
|
|
25
26
|
return;
|
|
26
27
|
await write(rel, content);
|
|
27
28
|
};
|
|
28
|
-
// Registry archetype (ADR 0176 §4.4): copy seed files from the bundle
|
|
29
|
+
// Registry archetype (M-ADR 0176 §4.4): copy seed files from the bundle
|
|
29
30
|
// cache FIRST — copy-and-own; the generic shape then fills only the
|
|
30
31
|
// gaps the seed didn't provide.
|
|
31
32
|
if (input.registryArchetype) {
|
|
@@ -34,6 +35,21 @@ export async function scaffoldDomain(input) {
|
|
|
34
35
|
filesWritten.push(join("docs", "domains", input.name, rel));
|
|
35
36
|
}
|
|
36
37
|
}
|
|
38
|
+
// The archetype's behaviour is seeded once into the behaviour store, as a change to agree, and cited from
|
|
39
|
+
// then on (archetype-behaviour.ts); the rows themselves are never copied into the domain.
|
|
40
|
+
let behaviourSkipped;
|
|
41
|
+
if (input.registryArchetype) {
|
|
42
|
+
const seeded = await seedArchetypeBehaviour({
|
|
43
|
+
targetRepo: input.targetRepo,
|
|
44
|
+
domain: input.name,
|
|
45
|
+
archetypeId: input.registryArchetype.id,
|
|
46
|
+
bundleName: input.registryArchetype.bundleName,
|
|
47
|
+
bundleCacheDir: input.registryArchetype.bundleCacheDir,
|
|
48
|
+
seeds: input.registryArchetype.seeds,
|
|
49
|
+
});
|
|
50
|
+
filesWritten.push(...seeded.filesWritten);
|
|
51
|
+
behaviourSkipped = seeded.skipped;
|
|
52
|
+
}
|
|
37
53
|
await writeIfMissing("README.md", renderDomainReadme(input));
|
|
38
54
|
await writeIfMissing("architecture/glossary.md", renderDomainGlossary(input));
|
|
39
55
|
await writeIfMissing("architecture/data-model.mermaid.md", renderDomainDataModel(input));
|
|
@@ -48,7 +64,7 @@ export async function scaffoldDomain(input) {
|
|
|
48
64
|
if (input.archetype === "integration") {
|
|
49
65
|
await write("architecture/interaction-patterns.md", renderIntegrationArchetype());
|
|
50
66
|
}
|
|
51
|
-
return { filesWritten };
|
|
67
|
+
return { filesWritten, ...(behaviourSkipped ? { behaviourSkipped } : {}) };
|
|
52
68
|
}
|
|
53
69
|
/**
|
|
54
70
|
* Copy a registry archetype's seed files from the bundle cache into the
|
|
@@ -63,6 +79,8 @@ async function copyRegistrySeeds(archetype, domainDir) {
|
|
|
63
79
|
const files = await walkFiles(seedRoot);
|
|
64
80
|
for (const abs of files) {
|
|
65
81
|
const rel = relative(seedRoot, abs);
|
|
82
|
+
if (rel === BEHAVIOUR_FILE)
|
|
83
|
+
continue; // seeded into the behaviour store and cited, never copied
|
|
66
84
|
const dst = join(domainDir, rel);
|
|
67
85
|
await mkdir(dirname(dst), { recursive: true });
|
|
68
86
|
await copyFile(abs, dst);
|
|
@@ -113,7 +131,7 @@ function titleize(slug) {
|
|
|
113
131
|
function renderDomainReadme(input) {
|
|
114
132
|
const title = titleize(input.name);
|
|
115
133
|
const intro = input.archetype === "integration"
|
|
116
|
-
? "Provider-specific integrations that bridge external systems to the platform via canonical contracts. Each integration is a separate `integration-product` SDD that registers into this domain (ADR 0136)."
|
|
134
|
+
? "Provider-specific integrations that bridge external systems to the platform via canonical contracts. Each integration is a separate `integration-product` SDD that registers into this domain (M-ADR 0136)."
|
|
117
135
|
: `TODO: one-paragraph description of the ${title} domain — the capability slice it owns and its boundary.`;
|
|
118
136
|
return ([
|
|
119
137
|
`# ${title} Domain`,
|
|
@@ -179,7 +197,7 @@ function renderDomainDataModel(input) {
|
|
|
179
197
|
return ([
|
|
180
198
|
`# ${title} Domain — Data Model`,
|
|
181
199
|
"",
|
|
182
|
-
"> Entity-relationship diagram for this domain (ADR 0142 — first-class catalogue entity).",
|
|
200
|
+
"> Entity-relationship diagram for this domain (M-ADR 0142 — first-class catalogue entity).",
|
|
183
201
|
"",
|
|
184
202
|
"```mermaid",
|
|
185
203
|
"erDiagram",
|
|
@@ -122,7 +122,7 @@ export async function resolveArchetype(targetRepo, reference) {
|
|
|
122
122
|
ok: false,
|
|
123
123
|
available,
|
|
124
124
|
error: available.length === 0
|
|
125
|
-
? `No archetype registries found in ${BUNDLE_CACHE_REL}/ — sync an archetype-bearing bundle first (ADR 0176).`
|
|
125
|
+
? `No archetype registries found in ${BUNDLE_CACHE_REL}/ — sync an archetype-bearing bundle first (M-ADR 0176).`
|
|
126
126
|
: `Archetype '${reference}' not found in cached registries. Available: ${known}`,
|
|
127
127
|
};
|
|
128
128
|
}
|
|
@@ -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
|
|
@@ -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,169 @@
|
|
|
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
|
+
//
|
|
15
|
+
// A digest must be a quoted string: YAML reads an unquoted all-digit value, or digits around one `e`, as a
|
|
16
|
+
// number, which would compare as a false stale. A narrowed or extended citation may say why in `reason`
|
|
17
|
+
// (`note` is read as the same). `not_adopted` records archetype requirements the estate has decided not to
|
|
18
|
+
// take, each with its reason and decision; every id must resolve, and none may be both cited and not adopted.
|
|
19
|
+
// A coverage line names the archetype requirements neither cited nor recorded as not adopted: the gaps, which
|
|
20
|
+
// are reported, not failed, since an estate may be part-way through adopting.
|
|
21
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import { parse as parseYaml } from "yaml";
|
|
24
|
+
import { cachedVersion, CITATIONS_FILE, loadArchetypeBehaviour, RELATIONS } from "../scaffold/archetype-behaviour.js";
|
|
25
|
+
import { discoverArchetypes } from "../scaffold/registry.js";
|
|
26
|
+
const REQ_RE = /^###\s+Requirement:\s*(.+?)\s*$/gm;
|
|
27
|
+
async function list(dir) {
|
|
28
|
+
try {
|
|
29
|
+
return (await readdir(dir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Every requirement title in the living specs and in open changes (a seeded change not yet archived counts). */
|
|
36
|
+
async function estateRequirements(repo) {
|
|
37
|
+
const titles = new Set();
|
|
38
|
+
const specFiles = [];
|
|
39
|
+
for (const d of await list(join(repo, "openspec", "specs")))
|
|
40
|
+
specFiles.push(join(repo, "openspec", "specs", d, "spec.md"));
|
|
41
|
+
for (const c of await list(join(repo, "openspec", "changes"))) {
|
|
42
|
+
if (c === "archive")
|
|
43
|
+
continue;
|
|
44
|
+
for (const d of await list(join(repo, "openspec", "changes", c, "specs"))) {
|
|
45
|
+
specFiles.push(join(repo, "openspec", "changes", c, "specs", d, "spec.md"));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
for (const f of specFiles) {
|
|
49
|
+
let text;
|
|
50
|
+
try {
|
|
51
|
+
text = await readFile(f, "utf8");
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
for (const m of text.matchAll(REQ_RE))
|
|
57
|
+
titles.add(m[1]);
|
|
58
|
+
}
|
|
59
|
+
return titles;
|
|
60
|
+
}
|
|
61
|
+
export async function checkArchetypeCitations(repo) {
|
|
62
|
+
const domains = await list(join(repo, "docs", "domains"));
|
|
63
|
+
const files = [];
|
|
64
|
+
for (const d of domains) {
|
|
65
|
+
const p = join(repo, "docs", "domains", d, CITATIONS_FILE);
|
|
66
|
+
try {
|
|
67
|
+
files.push([`docs/domains/${d}/${CITATIONS_FILE}`, await readFile(p, "utf8")]);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// no citations for this domain
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (files.length === 0)
|
|
74
|
+
return { exitCode: 0, stdout: "[N/A] no archetype citations (docs/domains/*/archetype-citations.yaml) in this repository\n" };
|
|
75
|
+
const archetypes = await discoverArchetypes(repo);
|
|
76
|
+
const titles = await estateRequirements(repo);
|
|
77
|
+
const lines = [];
|
|
78
|
+
let problems = 0;
|
|
79
|
+
let total = 0;
|
|
80
|
+
const bad = (tag, msg) => {
|
|
81
|
+
lines.push(`[${tag}] ${msg}`);
|
|
82
|
+
problems++;
|
|
83
|
+
};
|
|
84
|
+
for (const [rel, text] of files) {
|
|
85
|
+
let doc;
|
|
86
|
+
try {
|
|
87
|
+
doc = (parseYaml(text) ?? {});
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
bad("unresolved", `${rel}: does not parse: ${e.message.split("\n")[0]}`);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const archetype = archetypes.find((a) => a.id === doc.archetype && a.bundleName === doc.bundle);
|
|
94
|
+
if (!archetype) {
|
|
95
|
+
bad("unresolved", `${rel}: archetype ${String(doc.archetype)} of bundle ${String(doc.bundle)} is not in the bundle cache (sync the bundle)`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const reqs = await loadArchetypeBehaviour(archetype.bundleCacheDir, archetype.seeds);
|
|
99
|
+
const byId = new Map((reqs ?? []).map((r) => [r.id, r]));
|
|
100
|
+
const at = cachedVersion(archetype.bundleCacheDir) ?? "the cached bundle";
|
|
101
|
+
const cites = Array.isArray(doc.citations) ? doc.citations : [];
|
|
102
|
+
const notAdopted = Array.isArray(doc.not_adopted) ? doc.not_adopted : [];
|
|
103
|
+
if (cites.length === 0 && notAdopted.length === 0)
|
|
104
|
+
bad("unresolved", `${rel}: lists no citations`);
|
|
105
|
+
const citedIds = new Set(cites.map((c) => String(c.cites)));
|
|
106
|
+
for (const n of notAdopted) {
|
|
107
|
+
total++;
|
|
108
|
+
const tag = `${rel}: not adopted ${String(n.cites)}`;
|
|
109
|
+
if (!byId.has(String(n.cites))) {
|
|
110
|
+
bad("unresolved", `${tag}: ${archetype.bundleName} ${at} has no archetype requirement ${String(n.cites)}`);
|
|
111
|
+
}
|
|
112
|
+
else if (!n.reason || !n.decision) {
|
|
113
|
+
bad("unresolved", `${tag}: a non-adoption needs its reason and the decision that took it`);
|
|
114
|
+
}
|
|
115
|
+
else if (citedIds.has(String(n.cites))) {
|
|
116
|
+
bad("unresolved", `${tag}: it is also cited; an archetype requirement is either answered or not adopted`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
lines.push(`[ok] ${tag} (${String(n.reason)})`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (const c of cites) {
|
|
123
|
+
total++;
|
|
124
|
+
const tag = `${rel}: '${String(c.requirement)}' -> ${String(c.cites)}`;
|
|
125
|
+
const relation = String(c.relation);
|
|
126
|
+
if (typeof c.digest !== "string") {
|
|
127
|
+
bad("unresolved", `${tag}: digest must be a quoted string (YAML read ${String(c.digest)} as a ${typeof c.digest}); quote it`);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (!RELATIONS.includes(relation)) {
|
|
131
|
+
bad("unresolved", `${tag}: relation must be one of ${RELATIONS.join(", ")} (got ${relation})`);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (relation === "departed" && (!c.reason || !c.decision)) {
|
|
135
|
+
bad("unresolved", `${tag}: a departure needs its reason and the decision that took it`);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (relation !== "departed" && !titles.has(String(c.requirement))) {
|
|
139
|
+
bad("unresolved", `${tag}: this estate has no requirement titled so (living specs or open changes)`);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const src = byId.get(String(c.cites));
|
|
143
|
+
if (!src) {
|
|
144
|
+
bad("unresolved", `${tag}: ${archetype.bundleName} ${at} has no archetype requirement ${String(c.cites)}`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (relation === "departed") {
|
|
148
|
+
lines.push(`[ok] ${tag} (departed: ${String(c.reason)})`);
|
|
149
|
+
}
|
|
150
|
+
else if (src.digest !== String(c.digest)) {
|
|
151
|
+
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`);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
const why = c.reason ?? c.note;
|
|
155
|
+
lines.push(`[ok] ${tag} (${relation}${why && relation !== "derived" ? `: ${String(why)}` : ""})`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const answered = new Set([...citedIds, ...notAdopted.map((n) => String(n.cites))]);
|
|
159
|
+
const gaps = [...byId.keys()].filter((id) => !answered.has(id));
|
|
160
|
+
lines.push(`[coverage] ${rel}: ${byId.size - gaps.length} of ${byId.size} ${archetype.id} requirements answered ` +
|
|
161
|
+
`(${[...citedIds].filter((id) => byId.has(id)).length} cited, ${notAdopted.length} not adopted)` +
|
|
162
|
+
(gaps.length ? `; not yet answered: ${gaps.join(", ")}` : ""));
|
|
163
|
+
}
|
|
164
|
+
const verdict = problems
|
|
165
|
+
? `[FAIL] archetype citations: ${problems} of ${total} unresolved or stale`
|
|
166
|
+
: `[PASS] archetype citations: ${total} resolve, none stale`;
|
|
167
|
+
return { exitCode: problems ? 1 : 0, stdout: lines.join("\n") + "\n" + verdict + "\n" };
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=archetype-citations.js.map
|
|
@@ -20,6 +20,16 @@
|
|
|
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.
|
|
28
|
+
// 8. overlap: no requirement of a living spec is named by two open, agreed changes, whether added,
|
|
29
|
+
// modified, removed or renamed (M-ADR 0223 §4.10). Proposals may overlap.
|
|
30
|
+
// 9. schema: the store's config.yaml selects the method's sdd-change schema, and the store's copy of it
|
|
31
|
+
// (openspec/schemas/sdd-change/) is identical to the one the SDD's bundle shipped
|
|
32
|
+
// (docs/method/foundations/openspec/sdd-change/). Sync refreshes the copy (M-ADR 0223 §4.2).
|
|
23
33
|
// 6. scope: a baseline ADDS requirements; it REMOVES, MODIFIES or RENAMES only a requirement whose
|
|
24
34
|
// latest archived change was a baseline, which is how a corrective baseline withdraws
|
|
25
35
|
// what a baseline admitted wrongly. A requirement a judged change shipped is changed only
|
|
@@ -29,14 +39,18 @@
|
|
|
29
39
|
//
|
|
30
40
|
// OPENSPEC'S OWN VALIDATION is the separate validator `behaviour-store-openspec`, because it needs
|
|
31
41
|
// the OpenSpec CLI at OPENSPEC_VERSION (the Method Validation module installs it). Without a store
|
|
32
|
-
// both report [N/A], an empty population, never a pass.
|
|
42
|
+
// both report [N/A], an empty population, never a pass. It also archives each open, agreed change in a
|
|
43
|
+
// scratch copy of the store and validates the specs it would leave (M-ADR 0223 §4.5): a change's own
|
|
44
|
+
// --strict run exempts its MODIFIED requirements from the 500-character limit the specs are held to.
|
|
33
45
|
import { execFile } from "node:child_process";
|
|
34
|
-
import { access, readdir, readFile } from "node:fs/promises";
|
|
46
|
+
import { access, cp, mkdtemp, readdir, readFile, rm } from "node:fs/promises";
|
|
47
|
+
import { tmpdir } from "node:os";
|
|
35
48
|
import { basename, join } from "node:path";
|
|
36
49
|
import { promisify } from "node:util";
|
|
37
50
|
import { parseDocument } from "yaml";
|
|
38
51
|
import { observedIn, orderArchives, parseRequirements, parseSurfaces, renamedFrom, surfaceMatches } from "../behaviour-store/archive.js";
|
|
39
52
|
import { parseCatalogueCsv } from "../catalogue/csv-reader.js";
|
|
53
|
+
import { compareStoreSchema, SCHEMA_NAME, SHIPPED_SCHEMA_DIR, STORE_CONFIG, STORE_SCHEMA_DIR, storeSchemaName } from "../behaviour-store/schema.js";
|
|
40
54
|
const execFileAsync = promisify(execFile);
|
|
41
55
|
/** The OpenSpec release the method depends on (M-ADR 0223 ruling 2). The module installs exactly this. */
|
|
42
56
|
export const OPENSPEC_VERSION = "1.14.3";
|
|
@@ -236,7 +250,9 @@ export async function checkBehaviourStore(repo) {
|
|
|
236
250
|
const record = records.get(change);
|
|
237
251
|
if (tasks !== null && TASK_DONE_RE.test(tasks)) {
|
|
238
252
|
started++;
|
|
239
|
-
if (!record)
|
|
253
|
+
if (!record && recordFiles.some((f) => f.replace(/\.ya?ml$/, "") === change))
|
|
254
|
+
failures.push(`${dir}: has completed tasks but its agreement record (${AGREEMENTS_DIR}/${change}.yaml) is malformed; see its record failures`);
|
|
255
|
+
else if (!record)
|
|
240
256
|
failures.push(`${dir}: has completed tasks but no agreement record (${AGREEMENTS_DIR}/${change}.yaml)`);
|
|
241
257
|
else if (record.status !== "agreed")
|
|
242
258
|
failures.push(`${dir}: has completed tasks but its agreement record is ${record.status}, not agreed`);
|
|
@@ -259,6 +275,29 @@ export async function checkBehaviourStore(repo) {
|
|
|
259
275
|
}
|
|
260
276
|
lines.push(`agreement: ${open.length} open change(s), ${started} with completed tasks`);
|
|
261
277
|
lines.push(staleChecked === 0 ? "stale: vacuous (no open agreed change modifies, removes or renames a requirement)" : `stale: ${staleChecked} targeted living spec(s) checked against their agreed commits`);
|
|
278
|
+
// 8. overlap: two open, agreed changes never name the same requirement of one living spec (M-ADR 0223 §4.10).
|
|
279
|
+
// A name counts whether the delta adds, modifies, removes or renames it (both FROM and TO). Proposals may overlap.
|
|
280
|
+
const claimed = new Map();
|
|
281
|
+
let agreedOpen = 0;
|
|
282
|
+
for (const change of open) {
|
|
283
|
+
if (records.get(change)?.status !== "agreed")
|
|
284
|
+
continue;
|
|
285
|
+
agreedOpen++;
|
|
286
|
+
const specsDir = join(repo, STORE_DIR, "changes", change, "specs");
|
|
287
|
+
for (const spec of await dirs(specsDir)) {
|
|
288
|
+
const text = (await readText(join(specsDir, spec, "spec.md"))) ?? "";
|
|
289
|
+
const names = new Set([...parseRequirements(text).filter((r) => r.operation).map((r) => r.name), ...renamedFrom(text)]);
|
|
290
|
+
for (const name of names) {
|
|
291
|
+
const key = `${spec}::${name}`;
|
|
292
|
+
const other = claimed.get(key);
|
|
293
|
+
if (other)
|
|
294
|
+
failures.push(`${STORE_DIR}/changes/${change}: OVERLAP: requirement "${name}" of ${spec} is also named by agreed change ${other}; one waits for the other to archive, or the two become one change (M-ADR 0223 §4.10)`);
|
|
295
|
+
else
|
|
296
|
+
claimed.set(key, change);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
lines.push(agreedOpen < 2 ? `overlap: vacuous (${agreedOpen} open agreed change)` : `overlap: ${claimed.size} requirement name(s) across ${agreedOpen} open agreed changes`);
|
|
262
301
|
// 4. archived changes
|
|
263
302
|
const archived = await dirs(join(repo, STORE_DIR, "changes", "archive"));
|
|
264
303
|
let baselines = 0;
|
|
@@ -285,7 +324,31 @@ export async function checkBehaviourStore(repo) {
|
|
|
285
324
|
lines.push(`verdicts: ${archived.length} archived change(s), ${baselines} baseline(s), ${archived.length - baselines} needing a PASS verdict`);
|
|
286
325
|
// 5. surfaces, and 6. what each baseline may change, in archive order
|
|
287
326
|
const archiveRel = `${STORE_DIR}/changes/archive`;
|
|
288
|
-
const { order, ties } = await orderArchives(repo, archiveRel, archived);
|
|
327
|
+
const { order, ties, history } = await orderArchives(repo, archiveRel, archived);
|
|
328
|
+
// Same-day archives the history cannot order sit in one group. A scope verdict that a group peer could reverse
|
|
329
|
+
// (it touches the same requirement) depends on an order nobody knows, so it is unresolved, not a violation. Without
|
|
330
|
+
// git history (a worktree upload, a plain copy, an export) every same-day pair is such a group.
|
|
331
|
+
const peers = new Map();
|
|
332
|
+
for (const [a, b] of ties) {
|
|
333
|
+
const group = peers.get(a) ?? peers.get(b) ?? new Set();
|
|
334
|
+
group.add(a).add(b);
|
|
335
|
+
peers.set(a, group).set(b, group);
|
|
336
|
+
}
|
|
337
|
+
const touches = new Map();
|
|
338
|
+
for (const name of peers.keys()) {
|
|
339
|
+
const keys = new Set();
|
|
340
|
+
for (const feature of await dirs(join(repo, archiveRel, name, "specs"))) {
|
|
341
|
+
const text = (await readText(join(repo, archiveRel, name, "specs", feature, "spec.md"))) ?? "";
|
|
342
|
+
for (const r of parseRequirements(text))
|
|
343
|
+
if (r.operation)
|
|
344
|
+
keys.add(`${feature}::${r.name}`);
|
|
345
|
+
for (const from of renamedFrom(text))
|
|
346
|
+
keys.add(`${feature}::${from}`);
|
|
347
|
+
}
|
|
348
|
+
touches.set(name, keys);
|
|
349
|
+
}
|
|
350
|
+
const orderDecides = (name, key) => [...(peers.get(name) ?? [])].some((p) => p !== name && touches.get(p)?.has(key));
|
|
351
|
+
let scopeUnresolved = 0;
|
|
289
352
|
const servedBy = baselines > 0 ? await catalogueContracts(repo) : null;
|
|
290
353
|
let surfacesChecked = 0;
|
|
291
354
|
const lastTouch = new Map();
|
|
@@ -333,7 +396,11 @@ export async function checkBehaviourStore(repo) {
|
|
|
333
396
|
if (isBaseline) {
|
|
334
397
|
scopeChecked++;
|
|
335
398
|
const last = lastTouch.get(key);
|
|
336
|
-
if (last !== "baseline") {
|
|
399
|
+
if (last !== "baseline" && orderDecides(name, key)) {
|
|
400
|
+
scopeUnresolved++;
|
|
401
|
+
failures.push(`${dir}: scope unresolved for "${t}" (${feature}): the verdict depends on the order of same-day archives the history cannot give${history ? "" : " (no git history here: run from a clone)"}`);
|
|
402
|
+
}
|
|
403
|
+
else if (last !== "baseline") {
|
|
337
404
|
failures.push(`${dir}: a baseline changes "${t}" (${feature}), which ${last === "change" ? "a judged change last shipped" : "no earlier archived change admitted"}; a baseline may remove or modify only what a baseline admitted, and anything else takes a change (M-ADR 0222 §4.4)`);
|
|
338
405
|
}
|
|
339
406
|
}
|
|
@@ -352,8 +419,55 @@ export async function checkBehaviourStore(repo) {
|
|
|
352
419
|
const unseen = servedBy === null ? "" : `; repositories with no catalogue contracts: ${[...new Set(await observedInAll(repo, archiveRel, order))].filter((r) => (servedBy.get(r) ?? []).length === 0).join(", ") || "none"}`;
|
|
353
420
|
lines.push(baselines === 0 ? "surfaces: vacuous (no baseline)" : `surfaces: ${baselines} baseline(s), ${surfacesChecked} served contract(s) checked against catalogues/repo_contract.csv${unseen}`);
|
|
354
421
|
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`);
|
|
422
|
+
if (!history && archived.length > 0)
|
|
423
|
+
lines.push("order: no git history (not a git work tree, or git log failed), so same-day archives cannot be ordered");
|
|
424
|
+
if (scopeUnresolved > 0)
|
|
425
|
+
lines.push(`scope: ${scopeUnresolved} verdict(s) unresolved, depending on an order the history cannot give`);
|
|
355
426
|
for (const [a, b] of ties)
|
|
356
427
|
lines.push(`order: ${a} and ${b} were archived the same day and the history does not order them; ordered by name`);
|
|
428
|
+
// 7. living specs keyed by a catalogue feature
|
|
429
|
+
const livingSpecs = await dirs(join(repo, STORE_DIR, "specs"));
|
|
430
|
+
if (livingSpecs.length > 0) {
|
|
431
|
+
const featuresCsv = await readText(join(repo, "catalogues", "features.csv"));
|
|
432
|
+
if (featuresCsv === null) {
|
|
433
|
+
failures.push(`${STORE_DIR}/specs: living specs cannot be checked against catalogue features: catalogues/features.csv is missing; build the catalogue`);
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
const featureIds = new Set(parseCatalogueCsv(featuresCsv).rows.map((r) => r["id"] ?? ""));
|
|
437
|
+
const unkeyed = livingSpecs.filter((f) => !featureIds.has(f));
|
|
438
|
+
for (const f of unkeyed) {
|
|
439
|
+
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)`);
|
|
440
|
+
}
|
|
441
|
+
lines.push(`features: ${livingSpecs.length} living spec(s), ${livingSpecs.length - unkeyed.length} keyed by a catalogue feature`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
lines.push("features: vacuous (no living spec)");
|
|
446
|
+
}
|
|
447
|
+
// 9. schema: the store uses the method's sdd-change schema, and its copy is the one the SDD's bundle
|
|
448
|
+
// shipped (M-ADR 0223 §4.2). OpenSpec reads the copy, so an edited or stale copy changes what every
|
|
449
|
+
// change is asked for, with nothing else noticing.
|
|
450
|
+
const selected = await storeSchemaName(repo);
|
|
451
|
+
if (selected !== SCHEMA_NAME) {
|
|
452
|
+
failures.push(`${STORE_CONFIG}: ${selected === null ? "missing" : selected === "" ? "names no schema" : `selects schema "${selected}"`}; a behaviour store uses the method's ${SCHEMA_NAME} schema (M-ADR 0223 §4.2)`);
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
const c = await compareStoreSchema(repo);
|
|
456
|
+
if (!c.shipped) {
|
|
457
|
+
failures.push(`schema unresolved: this SDD holds no shipped ${SCHEMA_NAME} schema (${SHIPPED_SCHEMA_DIR}/); sync a method bundle that carries it`);
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
const fix = "sdd-cli sync refreshes it; put adopter guidance in the store's config.yaml rules, not in the schema";
|
|
461
|
+
for (const f of c.missing)
|
|
462
|
+
failures.push(`${STORE_SCHEMA_DIR}/${f}: missing from the store's copy of the schema; ${fix}`);
|
|
463
|
+
for (const f of c.differing)
|
|
464
|
+
failures.push(`${STORE_SCHEMA_DIR}/${f}: differs from the shipped ${SHIPPED_SCHEMA_DIR}/${f}; ${fix}`);
|
|
465
|
+
for (const f of c.extra)
|
|
466
|
+
failures.push(`${STORE_SCHEMA_DIR}/${f}: not in the shipped schema; ${fix}`);
|
|
467
|
+
const drift = c.missing.length + c.differing.length + c.extra.length;
|
|
468
|
+
lines.push(drift === 0 ? `schema: ${SCHEMA_NAME}, identical to the shipped copy` : `schema: ${SCHEMA_NAME}, ${drift} file(s) differ from the shipped copy`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
357
471
|
const population = recordFiles.length + open.length + archived.length;
|
|
358
472
|
const body = lines.map((l) => ` ${l}`).join("\n") + "\n";
|
|
359
473
|
if (failures.length > 0) {
|
|
@@ -361,7 +475,7 @@ export async function checkBehaviourStore(repo) {
|
|
|
361
475
|
}
|
|
362
476
|
if (population === 0)
|
|
363
477
|
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
|
|
478
|
+
return { exitCode: 0, stdout: body + "[PASS] behaviour store: records, agreement, stale, overlap, verdict, surfaces, scope, feature and schema checks hold\n" };
|
|
365
479
|
}
|
|
366
480
|
/** Per repository, the contracts the catalogue says it serves (repo_contract ⋈ contracts.spec_path); null when either file is missing. */
|
|
367
481
|
async function catalogueContracts(repo) {
|
|
@@ -452,6 +566,47 @@ export async function checkBehaviourStoreOpenspec(repo) {
|
|
|
452
566
|
if (r.code !== 0 && (t.failed ?? 0) === 0)
|
|
453
567
|
failures.push(`${label}: OpenSpec exited ${r.code} with no failed item reported`);
|
|
454
568
|
}
|
|
569
|
+
// Each open, agreed change, archived in a scratch copy: the specs it would leave must validate --strict.
|
|
570
|
+
let dryArchived = 0;
|
|
571
|
+
for (const change of (await dirs(join(repo, STORE_DIR, "changes"))).filter((d) => d !== "archive")) {
|
|
572
|
+
const rel = `${AGREEMENTS_DIR}/${change}.yaml`;
|
|
573
|
+
const text = await readText(join(repo, rel));
|
|
574
|
+
if (text === null || parseAgreementRecord(rel, text).record?.status !== "agreed")
|
|
575
|
+
continue;
|
|
576
|
+
const scratch = await mkdtemp(join(tmpdir(), "sdd-dry-archive-"));
|
|
577
|
+
try {
|
|
578
|
+
await cp(join(repo, STORE_DIR), join(scratch, STORE_DIR), { recursive: true });
|
|
579
|
+
const label = `specs after archiving ${change} (dry, in a copy)`;
|
|
580
|
+
const a = await runOpenspec(scratch, ["archive", change, "--yes", "--json"]);
|
|
581
|
+
if (a.code !== 0) {
|
|
582
|
+
const said = [...a.raw.matchAll(/"(?:message|error)":\s*"((?:[^"\\]|\\.)*)"/g)].map((m) => m[1]);
|
|
583
|
+
failures.push(`${label}: the change does not archive (exit ${a.code}): ${said.length > 0 ? said.join("; ") : a.raw.trim().split("\n").slice(-3).join(" | ")}`);
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
dryArchived++;
|
|
587
|
+
const v = await runOpenspec(scratch, ["validate", "--specs", "--strict", "--json", "--no-interactive"]);
|
|
588
|
+
const t = v.json?.summary?.totals;
|
|
589
|
+
if (!t) {
|
|
590
|
+
failures.push(`${label}: OpenSpec returned no report (exit ${v.code}): ${v.raw.trim().split("\n").slice(-3).join(" | ")}`);
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
lines.push(`${label}: ${t.items ?? 0} spec(s), ${t.passed ?? 0} passed, ${t.failed ?? 0} failed`);
|
|
594
|
+
for (const item of v.json?.items ?? []) {
|
|
595
|
+
if (item.valid)
|
|
596
|
+
continue;
|
|
597
|
+
for (const i of item.issues ?? [])
|
|
598
|
+
failures.push(`${label}: ${item.id}${i.path ? ` (${i.path})` : ""}: ${i.level} ${i.message}`);
|
|
599
|
+
if ((item.issues ?? []).length === 0)
|
|
600
|
+
failures.push(`${label}: ${item.id}: invalid`);
|
|
601
|
+
}
|
|
602
|
+
if (v.code !== 0 && (t.failed ?? 0) === 0)
|
|
603
|
+
failures.push(`${label}: OpenSpec exited ${v.code} with no failed spec reported`);
|
|
604
|
+
}
|
|
605
|
+
finally {
|
|
606
|
+
await rm(scratch, { recursive: true, force: true });
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
lines.push(`dry archive: ${dryArchived} open, agreed change(s) archived in a copy`);
|
|
455
610
|
const body = [` OpenSpec ${version}`, ...lines.map((l) => ` ${l}`)].join("\n") + "\n";
|
|
456
611
|
if (failures.length > 0)
|
|
457
612
|
return { exitCode: 1, stdout: body + failures.map((f) => `[FAIL] ${f}`).join("\n") + "\n" };
|
|
@@ -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
|
];
|
|
@@ -4,16 +4,16 @@ export function registerConformance(program) {
|
|
|
4
4
|
program
|
|
5
5
|
.command("conformance")
|
|
6
6
|
.description("Conformance-reverse: diff the declared surface (spec/binding/contract) against the observed " +
|
|
7
|
-
"surface (code) and report drift with PROVISIONAL verdicts (ADR 0180). Advisory by default — " +
|
|
7
|
+
"surface (code) and report drift with PROVISIONAL verdicts (M-ADR 0180). Advisory by default — " +
|
|
8
8
|
"disposition (code-defect vs spec-stale vs divergence) is a triage step, not this verb's call. " +
|
|
9
9
|
"Checks are derived from the spec-implementation binding (.sdd/implementation-binding.yaml, " +
|
|
10
|
-
"ADR 0181) when present; else .sdd/conformance-checks.yaml.")
|
|
10
|
+
"M-ADR 0181) when present; else .sdd/conformance-checks.yaml.")
|
|
11
11
|
.option("--target-repo <dir>", "target SDD repo", resolvePathOption, process.cwd())
|
|
12
12
|
.option("--config <path>", "explicit checks file (overrides the binding)")
|
|
13
13
|
.option("--code-root <dir>", "checkout root for implementation member repos (resolves @code/ sources)", resolvePathOption)
|
|
14
14
|
.option("--check <id>", "run only the named check")
|
|
15
15
|
.option("--json", "machine-readable report (CI ingestion)")
|
|
16
|
-
.option("--reconcile", "for binding-vs-reality checks, emit the binding fragment regenerated from the observed-set (ADR 0181 §2.3)")
|
|
16
|
+
.option("--reconcile", "for binding-vs-reality checks, emit the binding fragment regenerated from the observed-set (M-ADR 0181 §2.3)")
|
|
17
17
|
.option("--emit-finding <dir>", "write the conformance_finding catalogue CSV into <dir> (federates via aggregate)")
|
|
18
18
|
.option("--strict", "exit non-zero when any check needs triage (default: advisory, exit 0)")
|
|
19
19
|
.action(async (opts) => {
|
package/dist/verbs/list.js
CHANGED
|
@@ -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")
|
package/dist/verbs/render.js
CHANGED
|
@@ -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)")
|