@sdd-method/author-cli 0.7.0 → 0.8.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 +1 -1
- 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/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 +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/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/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 +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 +3 -3
- package/dist/lib/scaffold/archetype-behaviour.js +171 -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 +134 -0
- package/dist/lib/validate/behaviour-store.js +25 -1
- 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 +7 -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
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// `docs/domains/{domain}/product/{capability}.traceability.yaml`.
|
|
9
9
|
//
|
|
10
10
|
// Schema reference: docs/method/methods/capability-traceability-manifest-template.yaml
|
|
11
|
-
// in sdd-method (per ADR 0047 / capability-spec-template.md).
|
|
11
|
+
// in sdd-method (per M-ADR 0047 / capability-spec-template.md).
|
|
12
12
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
13
13
|
import { join, relative, sep } from "node:path";
|
|
14
14
|
import { parse as parseYaml } from "yaml";
|
|
@@ -84,6 +84,8 @@ export async function loadTraceabilityManifests(opts) {
|
|
|
84
84
|
}
|
|
85
85
|
throw err;
|
|
86
86
|
}
|
|
87
|
+
// A manifest dropped whole takes its capability's traceability with it, so
|
|
88
|
+
// each drop below is an error that fails the build (as for repo-configs).
|
|
87
89
|
const manifests = [];
|
|
88
90
|
for (const path of yamlPaths.sort()) {
|
|
89
91
|
const text = await readFile(path, "utf-8");
|
|
@@ -92,18 +94,18 @@ export async function loadTraceabilityManifests(opts) {
|
|
|
92
94
|
raw = parseYaml(text);
|
|
93
95
|
}
|
|
94
96
|
catch (err) {
|
|
95
|
-
opts.findings.add("convention", `failed to parse YAML at ${relative(opts.sddPath, path)}: ${err.message}
|
|
97
|
+
opts.findings.add("convention", `failed to parse YAML at ${relative(opts.sddPath, path)}: ${err.message} — manifest dropped`, "error", relative(opts.sddPath, path));
|
|
96
98
|
continue;
|
|
97
99
|
}
|
|
98
100
|
if (!raw || typeof raw !== "object") {
|
|
99
|
-
opts.findings.add("convention", `${relative(opts.sddPath, path)}: not an object —
|
|
101
|
+
opts.findings.add("convention", `${relative(opts.sddPath, path)}: not an object — manifest dropped`, "error", relative(opts.sddPath, path));
|
|
100
102
|
continue;
|
|
101
103
|
}
|
|
102
104
|
const obj = raw;
|
|
103
105
|
const cap = obj["capability"] ?? {};
|
|
104
106
|
const capId = String(cap["id"] ?? "").trim();
|
|
105
107
|
if (capId === "") {
|
|
106
|
-
opts.findings.add("convention", `${relative(opts.sddPath, path)}: missing capability.id
|
|
108
|
+
opts.findings.add("convention", `${relative(opts.sddPath, path)}: missing capability.id — manifest dropped`, "error", relative(opts.sddPath, path));
|
|
107
109
|
continue;
|
|
108
110
|
}
|
|
109
111
|
const relPath = relative(opts.sddPath, path);
|
|
@@ -53,7 +53,7 @@ export function findingRows(results, runDate) {
|
|
|
53
53
|
observed_not_declared: r.observedNotDeclared.length,
|
|
54
54
|
needs_triage: r.needsTriage,
|
|
55
55
|
run_date: runDate,
|
|
56
|
-
source_sdd_id: "", // populated by the aggregator per ADR 0134
|
|
56
|
+
source_sdd_id: "", // populated by the aggregator per M-ADR 0134
|
|
57
57
|
platform_sdd_id: "", // stamped at organisation tier per M-ADR 0208
|
|
58
58
|
};
|
|
59
59
|
lines.push(FINDING_COLUMNS.map((c) => csvCell(row[c])).join(","));
|
|
@@ -17,7 +17,7 @@ function resolveSource(targetRepo, source, codeRoot) {
|
|
|
17
17
|
}
|
|
18
18
|
const markdownProviderSignals = async (path, spec) => {
|
|
19
19
|
const out = new Set();
|
|
20
|
-
// `section` (ADR 0180 delta-pilot extension) scopes the scan to one heading block and accepts a plain
|
|
20
|
+
// `section` (M-ADR 0180 delta-pilot extension) scopes the scan to one heading block and accepts a plain
|
|
21
21
|
// "Field" header, so a check can read ONE entity table inside a multi-table data-model doc (e.g.
|
|
22
22
|
// data-model.mermaid.md "### Collection: orders") without duplicating the field list into a bespoke
|
|
23
23
|
// surface file. With no `section` the behaviour is byte-for-byte the legacy one (whole file, strict
|
|
@@ -499,7 +499,7 @@ function printReconcile(chk, r, schemaDir, out) {
|
|
|
499
499
|
if (!chk.reconcile || r.target !== "binding-vs-reality")
|
|
500
500
|
return;
|
|
501
501
|
const rc = chk.reconcile;
|
|
502
|
-
out(`\n--- reconcile ${r.id} -> regenerate \`${rc.block}\` in ${rc.binding} (ADR 0181 §2.3) ---\n`);
|
|
502
|
+
out(`\n--- reconcile ${r.id} -> regenerate \`${rc.block}\` in ${rc.binding} (M-ADR 0181 §2.3) ---\n`);
|
|
503
503
|
out(`# generated from the observed-set (${r.observed} schemas); the binding cannot drift if generated.\n`);
|
|
504
504
|
if (rc.note)
|
|
505
505
|
out(`# NOTE: ${rc.note}\n`);
|
|
@@ -513,7 +513,7 @@ function printReconcile(chk, r, schemaDir, out) {
|
|
|
513
513
|
}
|
|
514
514
|
}
|
|
515
515
|
async function planChecks(opts, stdout) {
|
|
516
|
-
// Resolution order: explicit --config wins; else the ADR 0181 binding (checks derived from it);
|
|
516
|
+
// Resolution order: explicit --config wins; else the M-ADR 0181 binding (checks derived from it);
|
|
517
517
|
// else the conventional .sdd/conformance-checks.yaml; else nothing to do.
|
|
518
518
|
if (!opts.config && hasBinding(opts.targetRepo)) {
|
|
519
519
|
const derived = await deriveChecksFromBinding(opts.targetRepo);
|
|
@@ -523,7 +523,7 @@ async function planChecks(opts, stdout) {
|
|
|
523
523
|
? resolveSource(opts.targetRepo, opts.config, opts.codeRoot)
|
|
524
524
|
: resolve(opts.targetRepo, ".sdd/conformance-checks.yaml");
|
|
525
525
|
if (!existsSync(configPath)) {
|
|
526
|
-
stdout(`[skip] no spec↔implementation binding (.sdd/implementation-binding.yaml or .sdd/implementation-bindings/, ADR 0181) and no ` +
|
|
526
|
+
stdout(`[skip] no spec↔implementation binding (.sdd/implementation-binding.yaml or .sdd/implementation-bindings/, M-ADR 0181) and no ` +
|
|
527
527
|
`checks file at ${configPath}\n`);
|
|
528
528
|
return null;
|
|
529
529
|
}
|
|
@@ -562,7 +562,7 @@ export async function runConformance(opts, stdout = (line) => process.stdout.wri
|
|
|
562
562
|
stdout(JSON.stringify({ checks: results, skipped }, null, 2) + "\n");
|
|
563
563
|
}
|
|
564
564
|
else {
|
|
565
|
-
stdout("Conformance-reverse (advisory; disposition is triage — ADR 0180 §2.3).\n");
|
|
565
|
+
stdout("Conformance-reverse (advisory; disposition is triage — M-ADR 0180 §2.3).\n");
|
|
566
566
|
for (const r of results) {
|
|
567
567
|
printHuman(r, stdout);
|
|
568
568
|
if (opts.reconcile) {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* source of truth).
|
|
7
7
|
*
|
|
8
8
|
* Replaces the v0.3.0 `generate-claude-md` verb whose rich-content
|
|
9
|
-
* output diverges from the ADR 0148 thin-pointer shape.
|
|
9
|
+
* output diverges from the M-ADR 0148 thin-pointer shape.
|
|
10
10
|
*/
|
|
11
11
|
import { access } from "node:fs/promises";
|
|
12
12
|
import { join } from "node:path";
|
|
@@ -36,7 +36,7 @@ export async function runGenerateBriefing(opts, stdout = (line) => process.stdou
|
|
|
36
36
|
// graceful skip downstream.
|
|
37
37
|
const templatesPresent = await fileExists(join(opts.targetDir, BRIEFING_TEMPLATE_ROOT, "agent-instructions.md.hbs"));
|
|
38
38
|
if (!templatesPresent) {
|
|
39
|
-
stdout(fail(`Briefing templates not found under ${BRIEFING_TEMPLATE_ROOT}/ — sync against a baseline that ships ADR 0148 templates first.`));
|
|
39
|
+
stdout(fail(`Briefing templates not found under ${BRIEFING_TEMPLATE_ROOT}/ — sync against a baseline that ships M-ADR 0148 templates first.`));
|
|
40
40
|
return { exitCode: 1 };
|
|
41
41
|
}
|
|
42
42
|
const result = await syncBriefingFiles({
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// The documented TypeQL subset — PARSER (M-ADR 0210 §4.2).
|
|
2
|
+
//
|
|
3
|
+
// This module reads the authored form of a fact-based model and nothing else: it knows
|
|
4
|
+
// the subset's grammar and refuses, with a located error, anything outside it. It knows
|
|
5
|
+
// nothing about catalogues, YAML or projections — that is `schema-projection.ts`. The split
|
|
6
|
+
// is deliberate: a store extending the subset extends the parser; the method's projection
|
|
7
|
+
// is one consumer of the parsed model among possible others.
|
|
8
|
+
//
|
|
9
|
+
// Fail-closed (M-ADR 0200 §4.3): a construct the parser does not recognise is a ParseError,
|
|
10
|
+
// never a skip. The five blindnesses measured in the prototype this replaces — `@values`,
|
|
11
|
+
// an `@card` form outside the subset, a role with two players, a value-type/family
|
|
12
|
+
// contradiction, a content change that must be visible — are each refused here or made
|
|
13
|
+
// visible to the projection, and the round-trip gate proves that with mutation controls
|
|
14
|
+
// before every verdict.
|
|
15
|
+
//
|
|
16
|
+
// Grammar (one declaration per line; `#` outside a string literal starts a comment):
|
|
17
|
+
// define
|
|
18
|
+
// attribute <label>, value <string|integer|double|boolean|date|datetime>;
|
|
19
|
+
// entity <label>[, owns <attr> [@key] [@card(1..1)|@card(0..1)|@card(0..)]][, plays <rel>:<role> [...]] ...;
|
|
20
|
+
// relation <label>[, relates <role> [@card(1..1)|@card(0..1)|@card(0..)]][, owns ...][, plays ...] ...;
|
|
21
|
+
// Meta rows (the second file):
|
|
22
|
+
// insert $m isa <meta-type>, has <key> <"string"|int|true|false>[, has ...];
|
|
23
|
+
export class ParseError extends Error {
|
|
24
|
+
constructor(message) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "ParseError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export const VALUE_TYPES = new Set(["string", "integer", "double", "boolean", "date", "datetime"]);
|
|
30
|
+
export const META_TYPES = new Set([
|
|
31
|
+
"schema-meta",
|
|
32
|
+
"catalogue-meta",
|
|
33
|
+
"column-meta",
|
|
34
|
+
"fk-meta",
|
|
35
|
+
"column-enum-meta",
|
|
36
|
+
"enum-meta",
|
|
37
|
+
"enum-value-meta",
|
|
38
|
+
"enum-value-property",
|
|
39
|
+
"coverage-meta",
|
|
40
|
+
"relationship-meta",
|
|
41
|
+
"vocabulary-meta",
|
|
42
|
+
"rule-meta",
|
|
43
|
+
]);
|
|
44
|
+
/** Meta keys whose values are lists (repeated `has` on one row). */
|
|
45
|
+
const LIST_KEYS = new Set(["m-external-for-profile", "m-reads"]);
|
|
46
|
+
function unlit(s, where) {
|
|
47
|
+
let out = "";
|
|
48
|
+
for (let i = 0; i < s.length; i++) {
|
|
49
|
+
const c = s[i];
|
|
50
|
+
if (c === "\\" && i + 1 < s.length) {
|
|
51
|
+
const n = s[i + 1];
|
|
52
|
+
const map = { n: "\n", t: "\t", '"': '"', "\\": "\\" };
|
|
53
|
+
const r = map[n];
|
|
54
|
+
if (r === undefined)
|
|
55
|
+
throw new ParseError(`${where}: bad escape \\${n}`);
|
|
56
|
+
out += r;
|
|
57
|
+
i++;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
out += c;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
/** A `#` outside a string literal starts a comment. */
|
|
66
|
+
export function stripComment(line) {
|
|
67
|
+
let out = "";
|
|
68
|
+
let q = false;
|
|
69
|
+
for (let i = 0; i < line.length; i++) {
|
|
70
|
+
const c = line[i];
|
|
71
|
+
if (c === '"' && (i === 0 || line[i - 1] !== "\\"))
|
|
72
|
+
q = !q;
|
|
73
|
+
if (c === "#" && !q)
|
|
74
|
+
break;
|
|
75
|
+
out += c;
|
|
76
|
+
}
|
|
77
|
+
return out.trim();
|
|
78
|
+
}
|
|
79
|
+
const CARD_RE = /^@card\((\d+)\.\.(\d*)\)$/;
|
|
80
|
+
function parseAnnotations(tokens, where) {
|
|
81
|
+
let key = false;
|
|
82
|
+
let required = false;
|
|
83
|
+
let multi = false;
|
|
84
|
+
let optional = false;
|
|
85
|
+
for (const t of tokens) {
|
|
86
|
+
if (t === "@key") {
|
|
87
|
+
key = true;
|
|
88
|
+
}
|
|
89
|
+
else if (t.startsWith("@card(")) {
|
|
90
|
+
const m = CARD_RE.exec(t);
|
|
91
|
+
if (!m)
|
|
92
|
+
throw new ParseError(`${where}: unparseable cardinality ${JSON.stringify(t)}`);
|
|
93
|
+
const lo = m[1];
|
|
94
|
+
const hi = m[2];
|
|
95
|
+
if (lo === "1" && hi === "1")
|
|
96
|
+
required = true;
|
|
97
|
+
else if (lo === "0" && hi === "")
|
|
98
|
+
multi = true;
|
|
99
|
+
else if (lo === "0" && hi === "1")
|
|
100
|
+
optional = true;
|
|
101
|
+
else
|
|
102
|
+
throw new ParseError(`${where}: cardinality ${t} cannot be projected onto the catalogue schema (only @card(1..1), @card(0..1) or @card(0..) are in the subset)`);
|
|
103
|
+
}
|
|
104
|
+
else if (t.startsWith("@values")) {
|
|
105
|
+
throw new ParseError(`${where}: @values(...) is not admitted in this projection — vocabularies bind through column_enums meta rows or a recorded absence (M-ADR 0210 §4.5)`);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
throw new ParseError(`${where}: unknown token ${JSON.stringify(t)}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { key, required, multi, optional };
|
|
112
|
+
}
|
|
113
|
+
function splitClauses(body, where) {
|
|
114
|
+
// refuse before the naive split can be fooled by a comma inside the parenthesis
|
|
115
|
+
if (body.includes("@values")) {
|
|
116
|
+
throw new ParseError(`${where}: @values(...) is not admitted in this projection (M-ADR 0210 §4.5)`);
|
|
117
|
+
}
|
|
118
|
+
return body.split(",").map((c) => c.trim());
|
|
119
|
+
}
|
|
120
|
+
const ATTR_RE = /^attribute ([a-z][a-z0-9-]*), value ([a-z]+)$/;
|
|
121
|
+
const TYPE_RE = /^(entity|relation) ([a-z][a-z0-9-]*)(?:, (.*))?$/;
|
|
122
|
+
/** Parse the type-system file. `fileLabel` names the file in error locations. */
|
|
123
|
+
export function parseTypes(text, fileLabel = "catalogue-schema.tql", options = {}) {
|
|
124
|
+
const singlePlayer = options.singlePlayer ?? true;
|
|
125
|
+
const attrs = new Map();
|
|
126
|
+
const entities = new Map();
|
|
127
|
+
const relations = new Map();
|
|
128
|
+
const seen = new Set();
|
|
129
|
+
const lines = text.split(/\r?\n/);
|
|
130
|
+
for (let ln = 1; ln <= lines.length; ln++) {
|
|
131
|
+
let line = stripComment(lines[ln - 1]);
|
|
132
|
+
if (line === "" || line === "define")
|
|
133
|
+
continue;
|
|
134
|
+
const where = `${fileLabel}:${ln}`;
|
|
135
|
+
if (!line.endsWith(";"))
|
|
136
|
+
throw new ParseError(`${where}: declaration must end with ';' — one declaration per line`);
|
|
137
|
+
line = line.slice(0, -1);
|
|
138
|
+
const am = ATTR_RE.exec(line);
|
|
139
|
+
if (am) {
|
|
140
|
+
const lab = am[1];
|
|
141
|
+
const vt = am[2];
|
|
142
|
+
if (!VALUE_TYPES.has(vt))
|
|
143
|
+
throw new ParseError(`${where}: unknown value type ${JSON.stringify(vt)}`);
|
|
144
|
+
if (seen.has(lab))
|
|
145
|
+
throw new ParseError(`${where}: duplicate declaration of ${JSON.stringify(lab)}`);
|
|
146
|
+
seen.add(lab);
|
|
147
|
+
attrs.set(lab, vt);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const tm = TYPE_RE.exec(line);
|
|
151
|
+
if (!tm)
|
|
152
|
+
throw new ParseError(`${where}: not in the subset: ${JSON.stringify(line.slice(0, 100))}`);
|
|
153
|
+
const kind = tm[1];
|
|
154
|
+
const lab = tm[2];
|
|
155
|
+
const body = tm[3];
|
|
156
|
+
if (seen.has(lab))
|
|
157
|
+
throw new ParseError(`${where}: duplicate declaration of ${JSON.stringify(lab)}`);
|
|
158
|
+
seen.add(lab);
|
|
159
|
+
const decl = { owns: new Map(), plays: new Map(), relates: new Map() };
|
|
160
|
+
for (const clause of body !== undefined ? splitClauses(body, where) : []) {
|
|
161
|
+
const w = clause.split(/\s+/).filter((x) => x !== "");
|
|
162
|
+
if (w.length === 0)
|
|
163
|
+
throw new ParseError(`${where}: empty clause`);
|
|
164
|
+
const head = w[0];
|
|
165
|
+
if (head === "owns") {
|
|
166
|
+
if (w.length < 2)
|
|
167
|
+
throw new ParseError(`${where}: owns without attribute`);
|
|
168
|
+
if (decl.owns.has(w[1]))
|
|
169
|
+
throw new ParseError(`${where}: ${lab} owns ${w[1]} twice`);
|
|
170
|
+
decl.owns.set(w[1], parseAnnotations(w.slice(2), where));
|
|
171
|
+
}
|
|
172
|
+
else if (head === "plays") {
|
|
173
|
+
if (w.length < 2 || !w[1].includes(":"))
|
|
174
|
+
throw new ParseError(`${where}: plays needs <relation>:<role>`);
|
|
175
|
+
if (decl.plays.has(w[1]))
|
|
176
|
+
throw new ParseError(`${where}: ${lab} plays ${w[1]} twice`);
|
|
177
|
+
decl.plays.set(w[1], parseAnnotations(w.slice(2), where));
|
|
178
|
+
}
|
|
179
|
+
else if (head === "relates") {
|
|
180
|
+
if (kind !== "relation")
|
|
181
|
+
throw new ParseError(`${where}: only a relation relates`);
|
|
182
|
+
if (w.length < 2)
|
|
183
|
+
throw new ParseError(`${where}: relates without role`);
|
|
184
|
+
if (decl.relates.has(w[1]))
|
|
185
|
+
throw new ParseError(`${where}: ${lab} relates ${w[1]} twice`);
|
|
186
|
+
decl.relates.set(w[1], parseAnnotations(w.slice(2), where));
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
throw new ParseError(`${where}: unknown clause ${JSON.stringify(head)}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
(kind === "entity" ? entities : relations).set(lab, decl);
|
|
193
|
+
}
|
|
194
|
+
// every owned attribute must be declared; every role must exist; a role has at most ONE player
|
|
195
|
+
const playersAll = new Map();
|
|
196
|
+
for (const [lab, d] of [...entities, ...relations]) {
|
|
197
|
+
for (const a of d.owns.keys()) {
|
|
198
|
+
if (!attrs.has(a))
|
|
199
|
+
throw new ParseError(`${lab} owns undeclared attribute ${JSON.stringify(a)}`);
|
|
200
|
+
}
|
|
201
|
+
for (const rr of d.plays.keys()) {
|
|
202
|
+
const idx = rr.indexOf(":");
|
|
203
|
+
const rel = rr.slice(0, idx);
|
|
204
|
+
const role = rr.slice(idx + 1);
|
|
205
|
+
const r = relations.get(rel);
|
|
206
|
+
if (!r)
|
|
207
|
+
throw new ParseError(`${lab} plays role of undeclared relation ${JSON.stringify(rel)}`);
|
|
208
|
+
if (!r.relates.has(role))
|
|
209
|
+
throw new ParseError(`${lab} plays undeclared role ${JSON.stringify(rr)}`);
|
|
210
|
+
const list = playersAll.get(rr) ?? [];
|
|
211
|
+
list.push(lab);
|
|
212
|
+
playersAll.set(rr, list);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const players = new Map();
|
|
216
|
+
for (const [rr, ps] of playersAll) {
|
|
217
|
+
if (ps.length > 1 && !singlePlayer)
|
|
218
|
+
continue;
|
|
219
|
+
if (ps.length > 1) {
|
|
220
|
+
throw new ParseError(`role ${rr} has ${ps.length} players (${ps.join(", ")}); the projection can carry exactly one and refuses to pick`);
|
|
221
|
+
}
|
|
222
|
+
players.set(rr, ps[0]);
|
|
223
|
+
}
|
|
224
|
+
return { attrs, entities, relations, players, playersAll };
|
|
225
|
+
}
|
|
226
|
+
const ROW_RE = /^insert \$m isa ([a-z][a-z0-9-]*), (.*);$/;
|
|
227
|
+
const TOK_RE = /has ([a-z][a-z0-9-]*) ("(?:[^"\\]|\\.)*"|-?\d+|true|false)/y;
|
|
228
|
+
export function parseMeta(text, fileLabel = "catalogue-schema-meta.tql", options = {}) {
|
|
229
|
+
const metaTypes = options.metaTypes ?? META_TYPES;
|
|
230
|
+
const rows = new Map();
|
|
231
|
+
const lines = text.split(/\r?\n/);
|
|
232
|
+
for (let ln = 1; ln <= lines.length; ln++) {
|
|
233
|
+
const line = stripComment(lines[ln - 1]);
|
|
234
|
+
if (line === "")
|
|
235
|
+
continue;
|
|
236
|
+
const where = `${fileLabel}:${ln}`;
|
|
237
|
+
const m = ROW_RE.exec(line);
|
|
238
|
+
if (!m)
|
|
239
|
+
throw new ParseError(`${where}: not in the subset: ${JSON.stringify(line.slice(0, 100))}`);
|
|
240
|
+
const typ = m[1];
|
|
241
|
+
const body = m[2];
|
|
242
|
+
if (!metaTypes.has(typ))
|
|
243
|
+
throw new ParseError(`${where}: unknown meta type ${JSON.stringify(typ)}`);
|
|
244
|
+
const acc = new Map();
|
|
245
|
+
let i = 0;
|
|
246
|
+
for (;;) {
|
|
247
|
+
TOK_RE.lastIndex = i;
|
|
248
|
+
const t = TOK_RE.exec(body);
|
|
249
|
+
if (!t)
|
|
250
|
+
throw new ParseError(`${where}: meta row body not in the subset at offset ${i}: ${JSON.stringify(body.slice(i, i + 60))}`);
|
|
251
|
+
const k = t[1];
|
|
252
|
+
const v = t[2];
|
|
253
|
+
let val;
|
|
254
|
+
if (v.startsWith('"'))
|
|
255
|
+
val = unlit(v.slice(1, -1), where);
|
|
256
|
+
else if (v === "true" || v === "false")
|
|
257
|
+
val = v === "true";
|
|
258
|
+
else
|
|
259
|
+
val = Number.parseInt(v, 10);
|
|
260
|
+
const list = acc.get(k) ?? [];
|
|
261
|
+
list.push(val);
|
|
262
|
+
acc.set(k, list);
|
|
263
|
+
i = TOK_RE.lastIndex;
|
|
264
|
+
if (i === body.length)
|
|
265
|
+
break;
|
|
266
|
+
if (body.startsWith(", ", i))
|
|
267
|
+
i += 2;
|
|
268
|
+
else
|
|
269
|
+
throw new ParseError(`${where}: expected ', has ...' at offset ${i}: ${JSON.stringify(body.slice(i, i + 40))}`);
|
|
270
|
+
}
|
|
271
|
+
const row = {};
|
|
272
|
+
for (const [k, vs] of acc)
|
|
273
|
+
row[k] = LIST_KEYS.has(k) ? vs : vs[0];
|
|
274
|
+
const list = rows.get(typ) ?? [];
|
|
275
|
+
list.push(row);
|
|
276
|
+
rows.set(typ, list);
|
|
277
|
+
}
|
|
278
|
+
return rows;
|
|
279
|
+
}
|
|
280
|
+
//# sourceMappingURL=tql-subset-parser.js.map
|
package/dist/lib/pack/schema.js
CHANGED
|
@@ -26,7 +26,7 @@ export const PackSeedSchema = z.object({
|
|
|
26
26
|
scope: z
|
|
27
27
|
.string()
|
|
28
28
|
.regex(/^[a-z0-9-]+$/, "scope must be lower-kebab-case slug"),
|
|
29
|
-
// Optional per ADR 0176 §4.6: absent = profile-agnostic pack.
|
|
29
|
+
// Optional per M-ADR 0176 §4.6: absent = profile-agnostic pack.
|
|
30
30
|
forwardBase: z
|
|
31
31
|
.object({
|
|
32
32
|
bundle: z.enum(KNOWN_FORWARD_BASES),
|
|
@@ -66,7 +66,7 @@ export const PackSeedSchema = z.object({
|
|
|
66
66
|
.array(z.string().min(1))
|
|
67
67
|
.min(1, "managed_paths must have at least one entry"),
|
|
68
68
|
forbiddenPaths: z.array(z.string()).optional(),
|
|
69
|
-
// ADR 0176 §4.3: scaffold-source path prefixes; each must cover at
|
|
69
|
+
// M-ADR 0176 §4.3: scaffold-source path prefixes; each must cover at
|
|
70
70
|
// least one managed_paths entry (checked in the superRefine below).
|
|
71
71
|
scaffoldSourcePaths: z.array(z.string().min(1)).optional(),
|
|
72
72
|
requiresPack: z
|
package/dist/lib/pack/seed.js
CHANGED
|
@@ -2,7 +2,7 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
import { dirname, isAbsolute, resolve } from "node:path";
|
|
3
3
|
import { parse as parseYaml } from "yaml";
|
|
4
4
|
/**
|
|
5
|
-
* Adopter pack seed file. Per ADR 0151 + context-pack-seed-schema.yaml.
|
|
5
|
+
* Adopter pack seed file. Per M-ADR 0151 + context-pack-seed-schema.yaml.
|
|
6
6
|
*
|
|
7
7
|
* Loaded by `sdd-cli pack build` and `sdd-cli pack validate` from
|
|
8
8
|
* `orchestration/distribution/{pack-name}-seed.yaml` in adopter repos.
|
|
@@ -120,7 +120,7 @@ async function loadSeedRecursive(absPath, visited) {
|
|
|
120
120
|
const seed = {
|
|
121
121
|
version: asString(own.version, "version", absPath),
|
|
122
122
|
scope: asString(own.scope, "scope", absPath),
|
|
123
|
-
// forward_base optional per ADR 0176 §4.6 (profile-agnostic packs omit it)
|
|
123
|
+
// forward_base optional per M-ADR 0176 §4.6 (profile-agnostic packs omit it)
|
|
124
124
|
...(own.forward_base && {
|
|
125
125
|
forwardBase: {
|
|
126
126
|
bundle: asString(own.forward_base.bundle, "forward_base.bundle", absPath),
|
|
@@ -36,7 +36,7 @@ export async function runPackValidate(opts, stdout = (s) => process.stdout.write
|
|
|
36
36
|
stdout(pass(`Seed valid: ${seed.scope} v${seed.version}`));
|
|
37
37
|
stdout(info(seed.forwardBase
|
|
38
38
|
? `Forward base: ${seed.forwardBase.bundle} >= ${seed.forwardBase.minimumVersion}`
|
|
39
|
-
: "Forward base: none (profile-agnostic pack, ADR 0176 §4.6)"));
|
|
39
|
+
: "Forward base: none (profile-agnostic pack, M-ADR 0176 §4.6)"));
|
|
40
40
|
if (seed.scaffoldSourcePaths) {
|
|
41
41
|
stdout(info(`Scaffold-source prefixes: ${seed.scaffoldSourcePaths.join(", ")}`));
|
|
42
42
|
}
|
|
@@ -10,7 +10,7 @@ import { parseRepoConfigFile } from "../sdd-index/repo-config.js";
|
|
|
10
10
|
*
|
|
11
11
|
* Repo-config parsing delegates to the shared sdd-index module so the
|
|
12
12
|
* `plan --pilot` path and the MCP dependency-graph tools see the same
|
|
13
|
-
* view. The richer ADR 0110 shape (package/version/stability) is
|
|
13
|
+
* view. The richer M-ADR 0110 shape (package/version/stability) is
|
|
14
14
|
* flattened back to path-strings here to preserve the legacy
|
|
15
15
|
* `RepoConfig` contract.
|
|
16
16
|
*/
|
package/dist/lib/repo-kind.js
CHANGED
|
@@ -2,7 +2,7 @@ import { readFile, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
/**
|
|
4
4
|
* Forward-base bundles set `.sdd-repo-kind` and qualify as the base
|
|
5
|
-
* layer that additive extensions can compose on top of (per ADR 0135).
|
|
5
|
+
* layer that additive extensions can compose on top of (per M-ADR 0135).
|
|
6
6
|
* The legacy self-contained `reverse-baseline` is retained as a
|
|
7
7
|
* forward base for compat with adopters who haven't migrated to the
|
|
8
8
|
* `reverse-extension-baseline` additive shape yet.
|
|
@@ -59,7 +59,7 @@ export function repoKindFromProfile(profile) {
|
|
|
59
59
|
/**
|
|
60
60
|
* Returns the profile a bundle authoritatively claims, or null for
|
|
61
61
|
* additive bundles that don't claim a profile of their own. Accepts
|
|
62
|
-
* arbitrary strings (adopter pack names per ADR 0151) and returns null
|
|
62
|
+
* arbitrary strings (adopter pack names per M-ADR 0151) and returns null
|
|
63
63
|
* for any name not in the closed sdd-method bundle set.
|
|
64
64
|
*/
|
|
65
65
|
export function profileFromBundleName(name) {
|
|
@@ -73,7 +73,7 @@ export function bundleNameFromProfile(profile) {
|
|
|
73
73
|
/**
|
|
74
74
|
* Forward-base bundles set `.sdd-repo-kind` and qualify as the base
|
|
75
75
|
* layer that additive extensions compose on top of. Additive bundles
|
|
76
|
-
* (sdd-method's `reverse-extension-baseline` and ADR 0151 adopter
|
|
76
|
+
* (sdd-method's `reverse-extension-baseline` and M-ADR 0151 adopter
|
|
77
77
|
* packs) return false. Accepts arbitrary strings; unknown names return
|
|
78
78
|
* false.
|
|
79
79
|
*/
|
|
@@ -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
|