@sdd-method/maintain-cli 0.8.0 → 0.10.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/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/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 +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/ci-binding/module.js +4 -2
- 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 +4 -12
- package/dist/lib/scaffold/archetype-behaviour.js +173 -0
- package/dist/lib/scaffold/registry.js +136 -0
- package/dist/lib/sync/adr-transitions.js +4 -2
- package/dist/lib/sync/briefing-merge.js +2 -2
- package/dist/lib/sync/bundle-cache.js +4 -4
- package/dist/lib/sync/index.js +39 -12
- 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 +169 -0
- package/dist/lib/validate/behaviour-store.js +161 -6
- 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,173 @@
|
|
|
1
|
+
// Archetype behaviour, seeded once and then cited.
|
|
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; `sdd-cli scaffold behaviour-store` creates one" };
|
|
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`; narrowed and extended may give",
|
|
158
|
+
"# a `reason`). Keep `digest` quoted. An archetype requirement this estate decides not to take goes under",
|
|
159
|
+
"# `not_adopted:` as {cites, reason, decision}. Checked by `sdd-cli validate --validator archetype-citations`: a",
|
|
160
|
+
"# changed source is reported until reviewed and re-pinned, and requirements neither cited nor not adopted are listed.",
|
|
161
|
+
"",
|
|
162
|
+
].join("\n");
|
|
163
|
+
const files = [
|
|
164
|
+
[join(changeDir, "proposal.md"), proposal],
|
|
165
|
+
[join(changeDir, "tasks.md"), tasks],
|
|
166
|
+
[join(changeDir, "specs", input.domain, "spec.md"), spec],
|
|
167
|
+
[citations, header + stringifyYaml(doc, { lineWidth: 0 })],
|
|
168
|
+
];
|
|
169
|
+
for (const [p, c] of files)
|
|
170
|
+
await writeAt(p, c);
|
|
171
|
+
return { filesWritten: files.map(([p]) => p.slice(input.targetRepo.length + 1)) };
|
|
172
|
+
}
|
|
173
|
+
//# 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
|
|
@@ -187,14 +187,16 @@ const CITATION_LINK = /\]\(([^)\s#]+)(?:#[^)\s]*)?\)/g;
|
|
|
187
187
|
*
|
|
188
188
|
* A citation is the moved path written literally (repo-relative), or a Markdown
|
|
189
189
|
* link whose target resolves to it from the citing file. Method-managed files are
|
|
190
|
-
* excluded: the bundle already ships them corrected.
|
|
190
|
+
* excluded: the bundle already ships them corrected. So is a moved file itself, at
|
|
191
|
+
* its old path: the sync renames it and replaces its content with the shipped copy.
|
|
191
192
|
*/
|
|
192
193
|
export async function findStrandedCitations(targetRepo, moved, managed, trackedTextFiles) {
|
|
193
194
|
if (moved.length === 0)
|
|
194
195
|
return [];
|
|
196
|
+
const leaving = new Set(moved.map((m) => m.fromPath));
|
|
195
197
|
const out = [];
|
|
196
198
|
for (const file of trackedTextFiles) {
|
|
197
|
-
if (managed.has(file))
|
|
199
|
+
if (managed.has(file) || leaving.has(file))
|
|
198
200
|
continue;
|
|
199
201
|
let text;
|
|
200
202
|
try {
|
|
@@ -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
|
@@ -16,6 +16,7 @@ import { syncBriefingFiles } from "./briefing-merge.js";
|
|
|
16
16
|
import { DEFAULT_AGENT_HARNESS } from "../agent-harness.js";
|
|
17
17
|
import { BLOCK_BEGIN, BLOCK_END } from "../init/ci-workflow.js";
|
|
18
18
|
import { blockSha256, CI_BINDINGS, DEFAULT_CI_BINDING, isCiBinding, parseCiRunner, syncCiBinding, } from "../ci-binding/index.js";
|
|
19
|
+
import { SCHEMA_NAME, STORE_SCHEMA_DIR, storeSchemaName, writeStoreSchema } from "../behaviour-store/schema.js";
|
|
19
20
|
import { BRIEFING_TEMPLATE_ROOT, WRAPPER_BLOCK_BEGIN, WRAPPER_BLOCK_END, briefingWrappers, } from "../briefing/index.js";
|
|
20
21
|
const SECTION_RULE = "═".repeat(59);
|
|
21
22
|
/**
|
|
@@ -75,7 +76,7 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
|
|
|
75
76
|
stdout(fail(`Missing payload file listed in MANAGED_PATHS.txt: ${missing[0]}`));
|
|
76
77
|
return { exitCode: 1 };
|
|
77
78
|
}
|
|
78
|
-
// ADR 0176 §4.3: split consumption modes. Scaffold-source paths and
|
|
79
|
+
// M-ADR 0176 §4.3: split consumption modes. Scaffold-source paths and
|
|
79
80
|
// the registry file extract to the bundle cache only; everything
|
|
80
81
|
// else materialises into the repo tree as before.
|
|
81
82
|
const { materialise: materialisePaths, cacheOnly: cacheOnlyPaths } = splitConsumptionModes(bundle.managedPaths, bundle.manifest.scaffoldSourcePaths);
|
|
@@ -155,7 +156,7 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
|
|
|
155
156
|
}
|
|
156
157
|
}
|
|
157
158
|
// Additive bundles require a forward base already in the lock.
|
|
158
|
-
// For adopter packs (ADR 0151) the requires_forward_base field
|
|
159
|
+
// For adopter packs (M-ADR 0151) the requires_forward_base field
|
|
159
160
|
// declares which sdd-method bundle (and minimum version) is needed.
|
|
160
161
|
const incomingBundleName = bundle.manifest.bundleName;
|
|
161
162
|
const incomingIsForward = bundle.manifest.isForwardBase;
|
|
@@ -184,7 +185,7 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
|
|
|
184
185
|
}
|
|
185
186
|
}
|
|
186
187
|
// 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.
|
|
188
|
+
// must be in the lock at a compatible version. Per M-ADR 0151 §4.2.
|
|
188
189
|
if (!incomingIsForward && bundle.manifest.requiresForwardBase) {
|
|
189
190
|
const err = checkRequiresForwardBase(prevLock, bundle.manifest.requiresForwardBase, incomingBundleName);
|
|
190
191
|
if (err !== null) {
|
|
@@ -193,7 +194,7 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
|
|
|
193
194
|
}
|
|
194
195
|
}
|
|
195
196
|
// Adopter pack requires_pack check: vertical extensions name a base
|
|
196
|
-
// pack they depend on. Per ADR 0151 §4.3.
|
|
197
|
+
// pack they depend on. Per M-ADR 0151 §4.3.
|
|
197
198
|
if (!incomingIsForward && bundle.manifest.requiresPack) {
|
|
198
199
|
const err = checkRequiresPack(prevLock, bundle.manifest.requiresPack, incomingBundleName);
|
|
199
200
|
if (err !== null) {
|
|
@@ -236,9 +237,19 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
|
|
|
236
237
|
...(migrateFrom?.managedPaths ?? []),
|
|
237
238
|
];
|
|
238
239
|
const withdrawnKeep = new Set(adrTransitions.protectedFromRemoval);
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
240
|
+
// A path the incoming bundle still manages is never a removal, whether it materialises into the tree or
|
|
241
|
+
// only into the bundle cache: the lock records cache-only paths too, so comparing against the
|
|
242
|
+
// materialised set alone scheduled every cache-only path as a removal on every sync.
|
|
243
|
+
// A cache-only path that IS in the tree was left there by a sync from before cache-only mode
|
|
244
|
+
// (M-ADR 0176 §4.3); it is removed, as before, but named as what it is.
|
|
245
|
+
const prevSet = new Set(prevPaths);
|
|
246
|
+
const cacheOnlyStrays = opts.delete
|
|
247
|
+
? cacheOnlyPaths.filter((p) => prevSet.has(p) && existsSync(join(opts.targetRepo, p)))
|
|
248
|
+
: [];
|
|
249
|
+
const removals = [
|
|
250
|
+
...computeRemovals(prevPaths, bundle.managedPaths, { noDelete: !opts.delete }),
|
|
251
|
+
...cacheOnlyStrays,
|
|
252
|
+
].filter((p) => !withdrawnKeep.has(p));
|
|
242
253
|
stdout(info(`Bundle: ${incomingBundleName} (version: ${bundle.manifest.bundleVersion})`));
|
|
243
254
|
stdout(info(`Incoming managed files: ${bundle.managedPaths.length}`));
|
|
244
255
|
if (cacheOnlyPaths.length > 0) {
|
|
@@ -290,8 +301,12 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
|
|
|
290
301
|
stdout(info(`Removals from previous sync of this bundle: ${removals.length}` +
|
|
291
302
|
(lockOnly > 0
|
|
292
303
|
? ` — ${removalsOnDisk.length} file(s) on disk to delete, ${lockOnly} lock entr(ies) only ` +
|
|
293
|
-
`(
|
|
304
|
+
`(no file at the path in this tree; nothing is deleted for these)`
|
|
294
305
|
: ` (all present on disk)`)));
|
|
306
|
+
if (cacheOnlyStrays.length > 0) {
|
|
307
|
+
stdout(info(`Of these, ${cacheOnlyStrays.length} cache-only path(s) were left in the tree by a sync from before cache-only mode; ` +
|
|
308
|
+
`the bundle cache holds them now, so the tree copies are removed`));
|
|
309
|
+
}
|
|
295
310
|
stdout(info(`ADR renames to apply: ${adrTransitions.renames.length}`));
|
|
296
311
|
stdout(info(`ADR transition warnings: ${adrTransitions.warnings.length}`));
|
|
297
312
|
if (adrTransitions.warnings.length > 0) {
|
|
@@ -303,7 +318,8 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
|
|
|
303
318
|
// each citation in the adopter's own documents to a moved path. Never
|
|
304
319
|
// rewritten; printed in dry-run too, so the figure a reviewer approves is the
|
|
305
320
|
// figure the run states.
|
|
306
|
-
|
|
321
|
+
// The managed set includes the paths this sync removes: they are method files too.
|
|
322
|
+
const stranded = await findStrandedCitations(opts.targetRepo, adrTransitions.movedPaths, new Set([...materialisePaths, ...removals]), await trackedTextFiles(opts.targetRepo));
|
|
307
323
|
if (stranded.length > 0) {
|
|
308
324
|
stdout(warn(`Citations to moved paths: ${stranded.length} — the adopter's own documents still name ` +
|
|
309
325
|
`a path the method moved. Update each (not rewritten automatically):`));
|
|
@@ -451,7 +467,7 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
|
|
|
451
467
|
for (const d of prunedDirs)
|
|
452
468
|
stdout(` - ${d}/\n`);
|
|
453
469
|
}
|
|
454
|
-
// ADR 0176 §4.2: cache non-forward-base bundles (the resolution
|
|
470
|
+
// M-ADR 0176 §4.2: cache non-forward-base bundles (the resolution
|
|
455
471
|
// substrate for scaffold --archetype). Forward bases are not cached
|
|
456
472
|
// — they carry no archetype registries and are large.
|
|
457
473
|
if (!bundle.manifest.isForwardBase) {
|
|
@@ -488,7 +504,7 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
|
|
|
488
504
|
isForwardBase: incomingIsForward,
|
|
489
505
|
managedPaths: [...bundle.managedPaths],
|
|
490
506
|
pathDigests,
|
|
491
|
-
// Adopter-pack provenance per ADR 0151 — fields only present for
|
|
507
|
+
// Adopter-pack provenance per M-ADR 0151 — fields only present for
|
|
492
508
|
// non-forward-base bundles produced by adopter pack builders.
|
|
493
509
|
...(bundle.manifest.producedByAdopter && {
|
|
494
510
|
producedByAdopter: bundle.manifest.producedByAdopter,
|
|
@@ -560,7 +576,7 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
|
|
|
560
576
|
}
|
|
561
577
|
for (const line of ci.log)
|
|
562
578
|
stdout(info(line));
|
|
563
|
-
// Briefing layer per ADR 0148: agent-instructions.md (unconditional
|
|
579
|
+
// Briefing layer per M-ADR 0148: agent-instructions.md (unconditional
|
|
564
580
|
// overwrite) + harness wrapper (marker-aware merge). Templates ship
|
|
565
581
|
// in the bundle's orchestration/templates/briefing/; gracefully skip
|
|
566
582
|
// when an older bundle without them is being synced.
|
|
@@ -577,6 +593,17 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
|
|
|
577
593
|
}
|
|
578
594
|
for (const line of briefingResult.log)
|
|
579
595
|
stdout(info(line));
|
|
596
|
+
// The store's copy of the method's change schema (M-ADR 0223 §4.2). The bundle ships it inert under
|
|
597
|
+
// docs/method/foundations/openspec/, and OpenSpec reads only the store's copy, so a store inside this
|
|
598
|
+
// SDD that selects the schema gets the shipped one. The copy is method-managed (the behaviour-store
|
|
599
|
+
// validator fails a drifted copy); adopter guidance belongs in the store's config.yaml rules.
|
|
600
|
+
if ((await storeSchemaName(opts.targetRepo)) === SCHEMA_NAME) {
|
|
601
|
+
const c = await writeStoreSchema(opts.targetRepo);
|
|
602
|
+
const changed = [...c.missing, ...c.differing, ...c.extra];
|
|
603
|
+
if (c.shipped && changed.length > 0) {
|
|
604
|
+
stdout(info(`Refreshed ${STORE_SCHEMA_DIR}/ from the shipped schema: ${changed.length} file(s) changed (${changed.join(", ")})`));
|
|
605
|
+
}
|
|
606
|
+
}
|
|
580
607
|
// Record the wrapper block and the module exactly as they now stand on disk, so the next sync
|
|
581
608
|
// can tell them untouched from edited (M-ADR 0218 §4.7), and the binding and the CLI source
|
|
582
609
|
// when they are not the defaults.
|
|
@@ -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
|