@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.
Files changed (100) hide show
  1. package/dist/author-program.js +2 -1
  2. package/dist/lib/agent-harness.js +2 -2
  3. package/dist/lib/behaviour-store/archive.js +28 -1
  4. package/dist/lib/behaviour-store/schema.js +85 -0
  5. package/dist/lib/briefing/index.js +1 -1
  6. package/dist/lib/briefing/render.js +1 -1
  7. package/dist/lib/briefing/variables.js +1 -1
  8. package/dist/lib/catalogue/application-archetype/vocabulary.js +2 -2
  9. package/dist/lib/catalogue/archetype/maps/android.yaml +2 -2
  10. package/dist/lib/catalogue/archetype/maps/swiftui.yaml +2 -2
  11. package/dist/lib/catalogue/archetype/maps/uikit.yaml +2 -2
  12. package/dist/lib/catalogue/archetype/maps/web-generic.yaml +2 -2
  13. package/dist/lib/catalogue/archetype/openui-archetype-vocabulary.yaml +3 -3
  14. package/dist/lib/catalogue/archetype/resolver.js +4 -4
  15. package/dist/lib/catalogue/archetype/wireframe.js +1 -1
  16. package/dist/lib/catalogue/build.js +36 -33
  17. package/dist/lib/catalogue/builders/adr-supersedes.js +3 -3
  18. package/dist/lib/catalogue/builders/adrs.js +3 -3
  19. package/dist/lib/catalogue/builders/c4-containers.js +2 -2
  20. package/dist/lib/catalogue/builders/c4-entity-rollups.js +1 -1
  21. package/dist/lib/catalogue/builders/c4-external-systems.js +2 -2
  22. package/dist/lib/catalogue/builders/c4-systems.js +1 -1
  23. package/dist/lib/catalogue/builders/capabilities.js +1 -1
  24. package/dist/lib/catalogue/builders/capability-domain-model.js +1 -1
  25. package/dist/lib/catalogue/builders/capability-spec-walker.js +2 -2
  26. package/dist/lib/catalogue/builders/capability-spec.js +4 -4
  27. package/dist/lib/catalogue/builders/contracts.js +3 -3
  28. package/dist/lib/catalogue/builders/data-store-mapping.js +2 -2
  29. package/dist/lib/catalogue/builders/domain-models.js +2 -2
  30. package/dist/lib/catalogue/builders/domains.js +3 -3
  31. package/dist/lib/catalogue/builders/entities-manifest.js +8 -8
  32. package/dist/lib/catalogue/builders/entities-mermaid.js +4 -4
  33. package/dist/lib/catalogue/builders/entity-persistence.js +1 -1
  34. package/dist/lib/catalogue/builders/entity-slug.js +3 -3
  35. package/dist/lib/catalogue/builders/event-catalog-walker.js +6 -6
  36. package/dist/lib/catalogue/builders/event-edges.js +6 -6
  37. package/dist/lib/catalogue/builders/events.js +3 -3
  38. package/dist/lib/catalogue/builders/evidence-class.js +1 -1
  39. package/dist/lib/catalogue/builders/feature-domain-model.js +1 -1
  40. package/dist/lib/catalogue/builders/feature-grounding-walker.js +2 -2
  41. package/dist/lib/catalogue/builders/feature-grounding.js +5 -5
  42. package/dist/lib/catalogue/builders/feature-subscription-channels.js +3 -3
  43. package/dist/lib/catalogue/builders/features.js +1 -1
  44. package/dist/lib/catalogue/builders/glossary-xref.js +4 -4
  45. package/dist/lib/catalogue/builders/integrations.js +4 -4
  46. package/dist/lib/catalogue/builders/persona-c4system.js +1 -1
  47. package/dist/lib/catalogue/builders/product-edges.js +1 -1
  48. package/dist/lib/catalogue/builders/product-platform-dependencies.js +2 -2
  49. package/dist/lib/catalogue/builders/products.js +5 -5
  50. package/dist/lib/catalogue/builders/repo-edges.js +12 -12
  51. package/dist/lib/catalogue/builders/repositories.js +11 -7
  52. package/dist/lib/catalogue/builders/requirements.js +110 -11
  53. package/dist/lib/catalogue/builders/screen-tree.js +6 -6
  54. package/dist/lib/catalogue/builders/screens.js +18 -18
  55. package/dist/lib/catalogue/builders/service-contract-consumes.js +1 -1
  56. package/dist/lib/catalogue/builders/service-entity-tables.js +3 -3
  57. package/dist/lib/catalogue/builders/services.js +5 -5
  58. package/dist/lib/catalogue/builders/system-external-dependency.js +2 -2
  59. package/dist/lib/catalogue/canonical-schema.yaml +138 -2
  60. package/dist/lib/catalogue/coverage-render.js +15 -15
  61. package/dist/lib/catalogue/csv-writer.js +1 -1
  62. package/dist/lib/catalogue/feature-grounding-aliases-loader.js +1 -1
  63. package/dist/lib/catalogue/findings.js +1 -1
  64. package/dist/lib/catalogue/fk-validator.js +2 -2
  65. package/dist/lib/catalogue/manifest-loader.js +3 -3
  66. package/dist/lib/catalogue/rubric-coverage-map.yaml +28 -0
  67. package/dist/lib/catalogue/schema-compat.js +2 -2
  68. package/dist/lib/catalogue/schema-loader.js +1 -1
  69. package/dist/lib/catalogue/screen-validation-loader.js +2 -2
  70. package/dist/lib/catalogue/service-architecture-parser.js +1 -1
  71. package/dist/lib/catalogue/traceability-loader.js +6 -4
  72. package/dist/lib/conformance/finding.js +1 -1
  73. package/dist/lib/conformance/index.js +5 -5
  74. package/dist/lib/generate-briefing/index.js +2 -2
  75. package/dist/lib/metamodel/tql-subset-parser.js +280 -0
  76. package/dist/lib/pack/schema.js +2 -2
  77. package/dist/lib/pack/seed.js +2 -2
  78. package/dist/lib/pack/validate.js +1 -1
  79. package/dist/lib/plan/read-sdd.js +1 -1
  80. package/dist/lib/repo-kind.js +4 -12
  81. package/dist/lib/scaffold/archetype-behaviour.js +173 -0
  82. package/dist/lib/scaffold/behaviour-store.js +60 -0
  83. package/dist/lib/scaffold/domain.js +24 -6
  84. package/dist/lib/scaffold/registry.js +1 -1
  85. package/dist/lib/sync/briefing-merge.js +2 -2
  86. package/dist/lib/sync/bundle-cache.js +4 -4
  87. package/dist/lib/sync/yaml-block-merge.js +1 -1
  88. package/dist/lib/validate/archetype-citations.js +169 -0
  89. package/dist/lib/validate/behaviour-store.js +161 -6
  90. package/dist/lib/validate/dispatch.js +8 -1
  91. package/dist/maintain-version.json +2 -2
  92. package/dist/verbs/conformance.js +3 -3
  93. package/dist/verbs/list.js +1 -1
  94. package/dist/verbs/render.js +1 -1
  95. package/dist/verbs/scaffold.js +36 -4
  96. package/dist/verify-program.js +2 -2
  97. package/dist/verify-version.json +2 -2
  98. package/package.json +2 -2
  99. package/scripts/validate-app-dependencies.sh +1 -1
  100. package/scripts/validate-integration-docs.sh +1 -1
@@ -7,7 +7,7 @@
7
7
  // referenced columns are external FKs (capability_id and service_id
8
8
  // are owned by the platform-root SDD, not by the consuming app SDD).
9
9
  // The per-SDD builder logs `[finding] external` for each unresolved
10
- // reference; the aggregator validates after merge per ADR 0134 §4.3.
10
+ // reference; the aggregator validates after merge per M-ADR 0134 §4.3.
11
11
  //
12
12
  // Profile gating: only application-product SDDs read this file. Other
13
13
  // profiles emit zero rows (no convention defined).
@@ -1,7 +1,7 @@
1
1
  // product_platform_dependencies builder — emits per-SDD rows recording a
2
2
  // product's consumption of a platform-side (capability, service) pair.
3
3
  //
4
- // Multi-source ingestion (ADR 0157 parity principle; same shape as ADR 0156
4
+ // Multi-source ingestion (M-ADR 0157 parity principle; same shape as M-ADR 0156
5
5
  // events ingestion). Two equal-status authoring sources, merged by PK:
6
6
  //
7
7
  // 1. FORWARD — `app-platform-dependencies.yaml` at the SDD root, the same
@@ -31,7 +31,7 @@
31
31
  // service (M-ADR 0192), `(product_id, target_capability_id, contract_path)`.
32
32
  // The aggregator subsequently joins synchronous rows against platform-side
33
33
  // `capability_service` to derive `cross_sdd_platform_capability` edges (per
34
- // ADR 0134, federation); async rows resolve via the capability leg (no
34
+ // M-ADR 0134, federation); async rows resolve via the capability leg (no
35
35
  // realiser service to join on) and skip the realiser-join.
36
36
  //
37
37
  // Cross-SDD references (target_capability_id, target_service_id) are
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * Products discoverer — emits the SDD-as-Product row from the manifest.
3
3
  *
4
- * Per ADR 0134 + catalogue-metamodel.md: every SDD is one Product. The
4
+ * Per M-ADR 0134 + catalogue-metamodel.md: every SDD is one Product. The
5
5
  * canonical builder emits ONE products.csv row per SDD — the SDD itself.
6
6
  * Sibling SDDs (apps, integrations) get their own products.csv rows in
7
7
  * their own builds; the aggregator merges by primary key.
8
8
  *
9
- * domain_id selection (per ADR 0136):
9
+ * domain_id selection (per M-ADR 0136):
10
10
  * - For platform-profile SDDs: manifest.domainId when declared, else
11
11
  * a sensible default for platform-profile categories (where a real
12
12
  * `platform` domain exists locally).
@@ -27,7 +27,7 @@
27
27
  import { PACKAGING_DEFAULT } from "../manifest-loader.js";
28
28
  /**
29
29
  * Default domain_id per category when the manifest doesn't declare one.
30
- * Per ADR 0136 only platform-profile categories carry a default — app
30
+ * Per M-ADR 0136 only platform-profile categories carry a default — app
31
31
  * and integration products legitimately have an empty domain_id.
32
32
  */
33
33
  const DEFAULT_DOMAIN_BY_CATEGORY = {
@@ -49,7 +49,7 @@ export function buildProducts(inputs) {
49
49
  }
50
50
  }
51
51
  else {
52
- // Per ADR 0136: app/integration/support products do not have a
52
+ // Per M-ADR 0136: app/integration/support products do not have a
53
53
  // primary domain. Even if the manifest declares one (typically the
54
54
  // umbrella name like "applications" or "integrations"), clear it
55
55
  // here so the products.domain_id FK reference does not dangle
@@ -58,7 +58,7 @@ export function buildProducts(inputs) {
58
58
  // so the override is traceable.
59
59
  domainId = "";
60
60
  if (m.domainId !== undefined && m.domainId.length > 0) {
61
- inputs.findings.add("inferred", `products: domain_id cleared (manifest declared "${m.domainId}") for profile=${m.profile} per ADR 0136 — app/integration/support products do not have a primary domain`);
61
+ inputs.findings.add("inferred", `products: domain_id cleared (manifest declared "${m.domainId}") for profile=${m.profile} per M-ADR 0136 — app/integration/support products do not have a primary domain`);
62
62
  }
63
63
  }
64
64
  // Lifecycle (schema 2.22.0): owner-declared in the manifest, default
@@ -1,6 +1,6 @@
1
1
  // Repo-edge builders — repo_capability, repo_contract, feature_repo.
2
2
  //
3
- // Per ADR 0157 (Forward-Declared / Reverse-Derived Parity for Edge Catalogues):
3
+ // Per M-ADR 0157 (Forward-Declared / Reverse-Derived Parity for Edge Catalogues):
4
4
  // both edges ingest from forward-method declared sources AND reverse-method
5
5
  // derived sources, merged by primary-key deduplication. The two paths are
6
6
  // equal-status per the parity principle; neither displaces the other.
@@ -32,20 +32,20 @@
32
32
  // services.primary_repo_id`. A service exposing a contract is implemented
33
33
  // by a repo (its primary_repo_id) — the repo "owns" the contract by
34
34
  // transitive inference. Works universally because primary_repo_id and
35
- // service_contract are core-schema populated. Added per ADR 0157 §4.2
35
+ // service_contract are core-schema populated. Added per M-ADR 0157 §4.2
36
36
  // amendment after an early-adopter 0/30 source_repo_id outcome.
37
37
  // repo_contract Path Y — `contracts.source_repo_id` denormalisation
38
- // (from OpenAPI `x-source-repo` hints per ADR 0110, or whatever
38
+ // (from OpenAPI `x-source-repo` hints per M-ADR 0110, or whatever
39
39
  // populates the column). Augments Path X when adopters carry the hint.
40
40
  // repo_capability Path X (universal) — `capability_service ⋈
41
41
  // services.primary_repo_id`. Works in both forward and reverse method
42
42
  // shapes because primary_repo_id is core-schema and capability_service
43
43
  // is heavily authored.
44
- // repo_capability Path Y (augments via ADR 0143 feature-grounding) —
44
+ // repo_capability Path Y (augments via M-ADR 0143 feature-grounding) —
45
45
  // `capability_feature ⋈ feature_repo`. Adopters who have adopted the
46
46
  // feature-grounding extractor get many additional rows.
47
47
  //
48
- // feature_repo: ADR 0143's feature-grounding extractor populates this in
48
+ // feature_repo: M-ADR 0143's feature-grounding extractor populates this in
49
49
  // build.ts (10a). The forward declared `contributes_to.artifact_type:
50
50
  // feature-set` path is documented in the metamodel but unused by any
51
51
  // observed adopter — left empty here pending first-adopter authoring.
@@ -68,7 +68,7 @@ import { normaliseAdditionalContextEntry, REPO_CONFIG_CONTRACT_KINDS, } from "./
68
68
  * identity-DEPENDENT, and so is this. A first review of D8 flagged the opposite
69
69
  * and was wrong.
70
70
  *
71
- * Caller supplies `existingIds` so this only ever ADDS: the ADR 0143
71
+ * Caller supplies `existingIds` so this only ever ADDS: the M-ADR 0143
72
72
  * feature-grounding extractor and the capability-spec walker own their rows and
73
73
  * are authoritative where they overlap.
74
74
  */
@@ -109,7 +109,7 @@ export function deriveFeatureRepoFromServices(inputs) {
109
109
  export function buildRepoEdges(inputs) {
110
110
  const repoCapability = collectRepoCapability(inputs);
111
111
  const repoContract = collectRepoContract(inputs);
112
- // feature_repo is populated later in build.ts by the ADR 0143
112
+ // feature_repo is populated later in build.ts by the M-ADR 0143
113
113
  // feature-grounding extractor; this builder doesn't contribute.
114
114
  const featureRepo = [];
115
115
  return { repoCapability, repoContract, featureRepo };
@@ -222,9 +222,9 @@ function collectRepoCapability(inputs) {
222
222
  missingPrimaryRepoSvcs++;
223
223
  }
224
224
  if (missingPrimaryRepoSvcs > 0) {
225
- inputs.findings.add("orphan", `repo_capability: ${missingPrimaryRepoSvcs} capability_service rows reference services with empty primary_repo_id — reverse-method Path X (capability_service ⋈ services.primary_repo_id) skipped those edges (per ADR 0157 §4.3)`);
225
+ inputs.findings.add("orphan", `repo_capability: ${missingPrimaryRepoSvcs} capability_service rows reference services with empty primary_repo_id — reverse-method Path X (capability_service ⋈ services.primary_repo_id) skipped those edges (per M-ADR 0157 §4.3)`);
226
226
  }
227
- // Reverse path Y — capability_feature ⋈ feature_repo (transitive via ADR 0143).
227
+ // Reverse path Y — capability_feature ⋈ feature_repo (transitive via M-ADR 0143).
228
228
  const featuresToRepos = new Map();
229
229
  for (const fr of featureRepoRows) {
230
230
  const fid = String(fr["feature_id"] ?? "");
@@ -299,7 +299,7 @@ function collectRepoContract(inputs) {
299
299
  }
300
300
  // Reverse path X (universal) — service_contract ⋈ services.primary_repo_id.
301
301
  // A service exposing a contract is implemented by a repo; the repo "owns"
302
- // the contract by transitive inference. Added per ADR 0157 §4.2 amendment
302
+ // the contract by transitive inference. Added per M-ADR 0157 §4.2 amendment
303
303
  // (an early adopter's source_repo_id was 0/30 populated, so
304
304
  // a derivation that depends only on it leaves them empty).
305
305
  const repoBySvcId = new Map();
@@ -323,10 +323,10 @@ function collectRepoContract(inputs) {
323
323
  emit(repoId, ctrId);
324
324
  }
325
325
  if (missingPrimaryRepoSvcs > 0) {
326
- inputs.findings.add("orphan", `repo_contract: ${missingPrimaryRepoSvcs} service_contract rows reference services with empty primary_repo_id — reverse-method Path X (service_contract ⋈ services.primary_repo_id) skipped those edges (per ADR 0157 §4.2)`);
326
+ inputs.findings.add("orphan", `repo_contract: ${missingPrimaryRepoSvcs} service_contract rows reference services with empty primary_repo_id — reverse-method Path X (service_contract ⋈ services.primary_repo_id) skipped those edges (per M-ADR 0157 §4.2)`);
327
327
  }
328
328
  // Reverse path Y — contracts.source_repo_id denormalisation.
329
- // Adopters who carry OpenAPI x-source-repo hints (per ADR 0110) or whose
329
+ // Adopters who carry OpenAPI x-source-repo hints (per M-ADR 0110) or whose
330
330
  // contracts builder otherwise populates source_repo_id get one row per
331
331
  // (source_repo_id, contract.id) for free.
332
332
  for (const c of contractRows) {
@@ -1,7 +1,7 @@
1
1
  // Repositories discoverer — emits `repositories.csv` rows from
2
2
  // `orchestration/repo-config/<domain>/<repo>.yaml` (recursive).
3
3
  //
4
- // Per ADR 0134 + Q7.1 resolution: `service_type` is constrained to the
4
+ // Per M-ADR 0134 + Q7.1 resolution: `service_type` is constrained to the
5
5
  // canonical `enums.service_type` set (api_service, worker, gateway,
6
6
  // frontend, mobile, library, infrastructure, documentation). Unknown
7
7
  // values log a `[finding] convention` and are kept verbatim — the FK
@@ -173,13 +173,13 @@ export async function buildRepositories(inputs) {
173
173
  inputs.findings.add("convention", `${cfg.relPath}: missing required \`domain\` field`);
174
174
  }
175
175
  else if (!inputs.knownDomainIds.has(domain)) {
176
- // Per ADR 0136: leave domain_id empty when the declared domain
176
+ // Per M-ADR 0136: leave domain_id empty when the declared domain
177
177
  // doesn't resolve locally (typical for app/integration SDDs whose
178
178
  // repo-config declares "applications"/"integrations").
179
179
  //
180
180
  // NOT `orphan`, and the argument is about TIER rather than a relabelling
181
181
  // of the same claim. A locally-unresolvable domain is two different
182
- // things and THIS TIER CANNOT TELL THEM APART: ADR 0136 externality (the
182
+ // things and THIS TIER CANNOT TELL THEM APART: M-ADR 0136 externality (the
183
183
  // domain is owned by a sibling SDD — correct, expected, nothing to fix)
184
184
  // or a genuinely dangling domain name (a typo). The discriminator lives
185
185
  // at estate scope, not here. Measured across two estates: 137
@@ -191,7 +191,7 @@ export async function buildRepositories(inputs) {
191
191
  // §4.2 names. `inferred` records the builder's documented resolution
192
192
  // (empty the column, keep the repository) without claiming a defect this
193
193
  // build cannot see.
194
- inputs.findings.add("inferred", `${cfg.relPath}: domain "${domain}" has no docs/domains/${domain}/ directory — domain_id left empty per ADR 0136. Whether that is external ownership or a dangling name is decidable only at estate scope`);
194
+ inputs.findings.add("inferred", `${cfg.relPath}: domain "${domain}" has no docs/domains/${domain}/ directory — domain_id left empty per M-ADR 0136. Whether that is external ownership or a dangling name is decidable only at estate scope`);
195
195
  resolvedDomain = "";
196
196
  }
197
197
  const loc = locByRepo.get(repoId);
@@ -252,19 +252,23 @@ async function loadRepoConfigs(sddPath, findings) {
252
252
  raw = parseYaml(text);
253
253
  }
254
254
  catch (err) {
255
- findings.add("convention", `failed to parse YAML at ${relative(sddPath, path)}: ${err.message}`);
255
+ findings.add("convention", `failed to parse YAML at ${relative(sddPath, path)}: ${err.message} — repository dropped`, "error", relative(sddPath, path));
256
256
  continue;
257
257
  }
258
258
  if (!raw || typeof raw !== "object") {
259
- findings.add("convention", `${relative(sddPath, path)}: not an object — skipped`);
259
+ findings.add("convention", `${relative(sddPath, path)}: not an object — repository dropped`, "error", relative(sddPath, path));
260
260
  continue;
261
261
  }
262
+ // A repo-config that cannot be read takes its repository, services and
263
+ // capability edges out of the catalogue with it. That is an error, not
264
+ // advice: as a warning it passed validation while the rows vanished, and
265
+ // only a downstream aggregate's FK check noticed.
262
266
  const parsed = RepoConfigSchema.safeParse(raw);
263
267
  if (!parsed.success) {
264
268
  const issues = parsed.error.issues
265
269
  .map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`)
266
270
  .join("; ");
267
- findings.add("convention", `${relative(sddPath, path)}: schema violation — ${issues}`);
271
+ findings.add("convention", `${relative(sddPath, path)}: schema violation — ${issues} — repository dropped`, "error", relative(sddPath, path));
268
272
  continue;
269
273
  }
270
274
  const repoFull = parsed.data.repo;
@@ -8,7 +8,13 @@
8
8
  // directory name, which is the catalogue feature id; plus one per requirement an open,
9
9
  // agreed change ADDS (stage agreed).
10
10
  // requirement_lineage each archived change, in archive order, whose delta ADDED, MODIFIED or RENAMED a
11
- // requirement that is current now. A change carrying baseline-evidence.md is a baseline.
11
+ // requirement that is current now, under any name a later RENAMED gave it. A change carrying
12
+ // baseline-evidence.md is a baseline. Statuses follow renames the same way.
13
+ // changes each change in the store, open or archived (M-ADR 0223 §4.2): its directory name, its
14
+ // undated name, kind, status (archived, or its agreement record's) and verdict.
15
+ // change_work_order per (change, repository): the record's work order, agreed or completed.
16
+ // change_work_item per (change, work item): the ids its proposal's `## Work Items` section lists, keyed by
17
+ // the undated name so the row survives archive.
12
18
  // requirement_status per (requirement, repository): observed from a baseline's `**Observed in:**` line;
13
19
  // declared-shipped from an archived change whose agreement record's verdict is PASS, through
14
20
  // its work orders' `implements` lists; agreed from an open change whose record is agreed.
@@ -20,7 +26,7 @@
20
26
  import { readdir, readFile, stat } from "node:fs/promises";
21
27
  import { join } from "node:path";
22
28
  import { parse as parseYaml } from "yaml";
23
- import { observedIn, orderArchives, parseRequirements } from "../../behaviour-store/archive.js";
29
+ import { observedIn, orderArchives, parseRequirements, renamePairs } from "../../behaviour-store/archive.js";
24
30
  export { observedIn, parseRequirements };
25
31
  const STORE = "openspec";
26
32
  const AGREEMENTS = join("docs", "agreements");
@@ -62,6 +68,17 @@ async function deltaOf(changeDir) {
62
68
  }
63
69
  return out;
64
70
  }
71
+ async function renamesOf(changeDir) {
72
+ const out = [];
73
+ for (const feature of await dirs(join(changeDir, "specs"))) {
74
+ const text = await readText(join(changeDir, "specs", feature, "spec.md"));
75
+ if (text === null)
76
+ continue;
77
+ for (const r of renamePairs(text))
78
+ out.push({ feature, ...r });
79
+ }
80
+ return out;
81
+ }
65
82
  async function agreementRecord(sddPath, change) {
66
83
  const text = await readText(join(sddPath, AGREEMENTS, `${change}.yaml`));
67
84
  if (text === null)
@@ -82,10 +99,33 @@ export async function archiveOrder(sddPath, names, findings) {
82
99
  }
83
100
  return order;
84
101
  }
102
+ const CHANGE_STATUSES = new Set(["proposed", "agreed", "superseded", "withdrawn"]);
103
+ /**
104
+ * The work-item ids a proposal's `## Work Items` section lists (M-ADR 0223 §4.2): one per bullet, the backticked
105
+ * id if there is one, else the bullet's first word. HTML comments (the template's guidance) are ignored.
106
+ */
107
+ export function parseWorkItems(proposal) {
108
+ const text = proposal.replace(/<!--[\s\S]*?-->/g, "");
109
+ const m = /^##\s+Work Items\s*$/im.exec(text);
110
+ if (!m)
111
+ return [];
112
+ const rest = text.slice(m.index + m[0].length);
113
+ const end = /^##\s/m.exec(rest);
114
+ const ids = [];
115
+ for (const line of (end ? rest.slice(0, end.index) : rest).split("\n")) {
116
+ const b = /^\s*[-*]\s+(.*)$/.exec(line);
117
+ if (!b)
118
+ continue;
119
+ const id = (/`([^`]+)`/.exec(b[1])?.[1] ?? b[1].split(/\s+/)[0] ?? "").replace(/[:,;]+$/, "").trim();
120
+ if (id && !ids.includes(id))
121
+ ids.push(id);
122
+ }
123
+ return ids;
124
+ }
85
125
  const rel = (sddPath, p) => p.slice(sddPath.length + 1).split("\\").join("/");
86
126
  export async function buildRequirements(inputs) {
87
127
  const { sddPath, findings } = inputs;
88
- const empty = { requirements: [], lineage: [], status: [] };
128
+ const empty = { requirements: [], lineage: [], status: [], changes: [], workOrders: [], workItems: [] };
89
129
  const store = join(sddPath, STORE);
90
130
  if (!(await isDir(store)))
91
131
  return empty;
@@ -115,6 +155,28 @@ export async function buildRequirements(inputs) {
115
155
  const events = [];
116
156
  const lineage = [];
117
157
  const unknownRepos = new Set();
158
+ const changes = [];
159
+ const workOrders = [];
160
+ const workItems = [];
161
+ // The change itself, its work orders and its work items (M-ADR 0223 §4.2).
162
+ const recordChange = async (id, name, kind, status, archivedOn, changeDir, record) => {
163
+ const verdict = record?.verdict?.result === "PASS" || record?.verdict?.result === "FAIL" ? record.verdict.result : "";
164
+ changes.push({ id, name, change_kind: kind, status, archived_on: archivedOn, verdict, source_sdd_id: "", platform_sdd_id: "" });
165
+ for (const wo of record?.work_orders ?? []) {
166
+ if (!wo.repo)
167
+ continue;
168
+ if (!inputs.knownRepoIds.has(wo.repo)) {
169
+ unknownRepos.add(wo.repo);
170
+ continue;
171
+ }
172
+ const woStatus = wo.status === "agreed" || wo.status === "completed" ? wo.status : verdict === "PASS" && status === "archived" ? "completed" : "agreed";
173
+ workOrders.push({ id: `${id}__${wo.repo}`, change_id: id, repo_id: wo.repo, status: woStatus, source_sdd_id: "", platform_sdd_id: "" });
174
+ }
175
+ const proposal = await readText(join(changeDir, "proposal.md"));
176
+ for (const item of proposal === null ? [] : parseWorkItems(proposal)) {
177
+ workItems.push({ id: `${name}__${item}`, change_id: id, change_name: name, work_item_id: item, source_sdd_id: "", platform_sdd_id: "" });
178
+ }
179
+ };
118
180
  const pushEvent = (e) => {
119
181
  if (!inputs.knownRepoIds.has(e.repo)) {
120
182
  unknownRepos.add(e.repo);
@@ -123,21 +185,25 @@ export async function buildRequirements(inputs) {
123
185
  if (requirements.has(e.requirementId))
124
186
  events.push(e);
125
187
  };
126
- // 2. Archived changes, in archive order.
127
- for (const dir of await archiveOrder(sddPath, await dirs(join(store, "changes", "archive")), findings)) {
188
+ // 2. Archived changes, in archive order. A requirement is named as it was when each change archived, so its
189
+ // rows are carried forward through every later RENAMED (and one in the same change) to the name it has now;
190
+ // otherwise a rename would read as a requirement born at the rename, with no baseline.
191
+ const archived = [];
192
+ const renames = [];
193
+ const archives = await archiveOrder(sddPath, await dirs(join(store, "changes", "archive")), findings);
194
+ for (const [index, dir] of archives.entries()) {
128
195
  const changeDir = join(store, "changes", "archive", dir);
196
+ renames.push(await renamesOf(changeDir));
129
197
  const change = dir.replace(/^\d{4}-\d{2}-\d{2}-/, "");
130
198
  const archivedOn = /^(\d{4}-\d{2}-\d{2})-/.exec(dir)?.[1] ?? "";
131
199
  const evidence = await readText(join(changeDir, "baseline-evidence.md"));
132
200
  const kind = evidence !== null ? "baseline" : "change";
201
+ await recordChange(dir, change, kind, "archived", archivedOn, changeDir, await agreementRecord(sddPath, change));
133
202
  const delta = await deltaOf(changeDir);
134
203
  for (const d of delta) {
135
204
  if (d.operation === "REMOVED")
136
205
  continue;
137
- const requirementId = `${d.feature}::${d.name}`;
138
- if (!requirements.has(requirementId))
139
- continue;
140
- lineage.push({ id: `${requirementId}__${dir}`, requirement_id: requirementId, change_id: dir, change_kind: kind, operation: d.operation, archived_on: archivedOn, source_sdd_id: "", platform_sdd_id: "" });
206
+ archived.push({ index, feature: d.feature, name: d.name, lineage: { change_id: dir, change_kind: kind, operation: d.operation, archived_on: archivedOn, source_sdd_id: "", platform_sdd_id: "" } });
141
207
  }
142
208
  if (evidence !== null) {
143
209
  const repos = observedIn(evidence);
@@ -147,7 +213,7 @@ export async function buildRequirements(inputs) {
147
213
  for (const repo of repos)
148
214
  for (const d of delta)
149
215
  if (d.operation !== "REMOVED")
150
- pushEvent({ requirementId: `${d.feature}::${d.name}`, repo, status: "observed", basis: dir });
216
+ archived.push({ index, feature: d.feature, name: d.name, event: { repo, status: "observed", basis: dir } });
151
217
  continue;
152
218
  }
153
219
  const record = await agreementRecord(sddPath, change);
@@ -162,13 +228,43 @@ export async function buildRequirements(inputs) {
162
228
  findings.add("convention", `${AGREEMENTS}/${change}.yaml: work order for ${wo.repo} implements "${name}", which the change's delta does not name`);
163
229
  }
164
230
  for (const d of match)
165
- pushEvent({ requirementId: `${d.feature}::${d.name}`, repo: wo.repo, status: "declared-shipped", basis: dir });
231
+ archived.push({ index, feature: d.feature, name: d.name, event: { repo: wo.repo, status: "declared-shipped", basis: dir } });
166
232
  }
167
233
  }
168
234
  }
235
+ const current = (feature, name, from) => {
236
+ let n = name;
237
+ for (let j = from; j < renames.length; j++)
238
+ for (const r of renames[j])
239
+ if (r.feature === feature && r.from === n)
240
+ n = r.to;
241
+ return `${feature}::${n}`;
242
+ };
243
+ // One lineage row per (requirement, change): a change that renames and modifies a requirement records MODIFIED,
244
+ // the behaviour change, and its delta still shows the rename.
245
+ const lineageById = new Map();
246
+ for (const a of archived) {
247
+ const requirementId = current(a.feature, a.name, a.index);
248
+ if (a.event)
249
+ pushEvent({ requirementId, ...a.event });
250
+ else if (a.lineage && requirements.has(requirementId)) {
251
+ const id = `${requirementId}__${a.lineage["change_id"]}`;
252
+ if (lineageById.get(id)?.["operation"] === "MODIFIED")
253
+ continue;
254
+ lineageById.set(id, { id, requirement_id: requirementId, ...a.lineage });
255
+ }
256
+ }
257
+ lineage.push(...lineageById.values());
169
258
  // 3. Open changes whose agreement record is agreed.
170
259
  for (const dir of (await dirs(join(store, "changes"))).filter((d) => d !== "archive")) {
171
260
  const record = await agreementRecord(sddPath, dir);
261
+ const openKind = (await readText(join(store, "changes", dir, "baseline-evidence.md"))) !== null ? "baseline" : "change";
262
+ let openStatus = record?.status ?? "proposed";
263
+ if (!CHANGE_STATUSES.has(openStatus)) {
264
+ findings.add("convention", `${AGREEMENTS}/${dir}.yaml: status "${openStatus}" is not a change status; catalogued as proposed`);
265
+ openStatus = "proposed";
266
+ }
267
+ await recordChange(dir, dir, openKind, openStatus, "", join(store, "changes", dir), record);
172
268
  if (record?.status !== "agreed")
173
269
  continue;
174
270
  const delta = await deltaOf(join(store, "changes", dir));
@@ -203,6 +299,9 @@ export async function buildRequirements(inputs) {
203
299
  requirements: [...requirements.values()].sort(byId),
204
300
  lineage,
205
301
  status: [...status.values()].sort(byId),
302
+ changes: changes.sort(byId),
303
+ workOrders: workOrders.sort(byId),
304
+ workItems: workItems.sort(byId),
206
305
  };
207
306
  }
208
307
  //# sourceMappingURL=requirements.js.map
@@ -1,15 +1,15 @@
1
- // Screen composition-tree analysis for openui-lang blocks (per ADR 0168).
1
+ // Screen composition-tree analysis for openui-lang blocks (per M-ADR 0168).
2
2
  //
3
3
  // The screens builder (screens.ts) lowers a ```openui-lang block into catalogue
4
- // rows by flat token-scanning. ADR 0168 adds a *fidelity floor*: the block must
4
+ // rows by flat token-scanning. M-ADR 0168 adds a *fidelity floor*: the block must
5
5
  // be a faithful tree, not a flat / empty / mis-rooted "widget bag". That needs
6
6
  // the structural shape the flat scan throws away — so this module parses the
7
7
  // block into a tree and computes the metrics the depth findings reason about.
8
8
  //
9
9
  // Pure functions, no I/O. The metrics reason about TREE SHAPE only — never about
10
- // any component vocabulary (adopter-agnostic by construction, ADR 0168 §4.5).
10
+ // any component vocabulary (adopter-agnostic by construction, M-ADR 0168 §4.5).
11
11
  //
12
- // Grammar recap (ADR 0166 / openui-lang-grammar-reference.md): one
12
+ // Grammar recap (M-ADR 0166 / openui-lang-grammar-reference.md): one
13
13
  // `identifier = Expression` statement per line; the first statement is `root`;
14
14
  // composition is by identifier reference inside list literals. A component is a
15
15
  // Capitalised call head (`Name(`); local references are lowercase ids that are
@@ -73,7 +73,7 @@ function parseArgsActions(rhs) {
73
73
  }
74
74
  /**
75
75
  * Strip openui-lang inline comments: from the first *unquoted* `#` to
76
- * end-of-line, per line (ADR 0168 §4.4, defect g). A `Capitalised(` token inside
76
+ * end-of-line, per line (M-ADR 0168 §4.4, defect g). A `Capitalised(` token inside
77
77
  * a comment must not be lexed as a component. `#` inside a string literal is
78
78
  * preserved.
79
79
  */
@@ -113,7 +113,7 @@ function parseStatements(cleanBlock) {
113
113
  const rhs = m[2];
114
114
  // A component statement's RHS is a call (`Text("Q4")`) or a reference — never a
115
115
  // bare string literal. A line like `action="basket-updated"` (the block-level
116
- // screen_action messaging contract, ADR 0166) matches the statement shape with
116
+ // screen_action messaging contract, M-ADR 0166) matches the statement shape with
117
117
  // id=`action`; skip it so it isn't lexed as a phantom component (and so a node
118
118
  // carrying an `action=` attr doesn't pick it up as a spurious child).
119
119
  if (/^["']/.test(rhs))
@@ -1,4 +1,4 @@
1
- // Screens builder — emits the UI screens catalogue family (per ADR 0166):
1
+ // Screens builder — emits the UI screens catalogue family (per M-ADR 0166):
2
2
  // screens, ui_components, screen_component, screen_feature,
3
3
  // persona_screen, screen_action.
4
4
  //
@@ -7,7 +7,7 @@
7
7
  // ---
8
8
  // screen: <slug> # optional; defaults to filename stem
9
9
  // route: /path # optional
10
- // status: working # optional (ADR 0165 maturity vocabulary)
10
+ // status: working # optional (M-ADR 0165 maturity vocabulary)
11
11
  // persona_access: [<persona-slug>, ...] # -> persona_screen
12
12
  // realises: [<feature-slug>, ...] # -> screen_feature
13
13
  // ---
@@ -25,7 +25,7 @@
25
25
  // toolkit projects an app's router + component tree into the same shape (the
26
26
  // upstream step that produces these artefacts).
27
27
  //
28
- // Per ADR 0135 the rows are identical regardless of authoring direction; only
28
+ // Per M-ADR 0135 the rows are identical regardless of authoring direction; only
29
29
  // the `source` discriminator differs (screen-spec vs screen-extraction).
30
30
  //
31
31
  // Application-product profile. Like personas, the walker reads `screens/` at
@@ -36,9 +36,9 @@ import { basename, join } from "node:path";
36
36
  import { parse as parseYaml } from "yaml";
37
37
  import { analyzeComposition, maskStringLiterals, stripBlockComments } from "./screen-tree.js";
38
38
  import { isContainerArchetype } from "../archetype/resolver.js";
39
- /** Method default for the flat-composition threshold (ADR 0168 §4.2). */
39
+ /** Method default for the flat-composition threshold (M-ADR 0168 §4.2). */
40
40
  const DEFAULT_FLAT_THRESHOLD = 6;
41
- /** The only maturity value that asserts a faithful, complete composition (ADR 0168 §4.3). */
41
+ /** The only maturity value that asserts a faithful, complete composition (M-ADR 0168 §4.3). */
42
42
  const COMPLETENESS_STATUS = "working";
43
43
  const SCREENS_DIR_REL = "screens";
44
44
  /** Subdir of screens/ holding rendered wireframe artifacts — never screen specs. */
@@ -72,7 +72,7 @@ async function collectScreenSpecFiles(dir) {
72
72
  await walk("");
73
73
  return out.sort();
74
74
  }
75
- // Recognised standard maturity vocabulary (ADR 0165); other values pass through.
75
+ // Recognised standard maturity vocabulary (M-ADR 0165); other values pass through.
76
76
  const STANDARD_MATURITY = new Set(["working", "provisional", "unknown", "absent"]);
77
77
  export async function buildScreens(inputs) {
78
78
  const screens = [];
@@ -105,7 +105,7 @@ export async function buildScreens(inputs) {
105
105
  }
106
106
  const name = extractH1(text) || (typeof fm.name === "string" ? fm.name : stem);
107
107
  const status = typeof fm.status === "string" && fm.status !== "" ? maturity(fm.status, inputs.findings, screenId) : "";
108
- // Direction discriminator (ADR 0135): forward authoring is `screen-spec`,
108
+ // Direction discriminator (M-ADR 0135): forward authoring is `screen-spec`,
109
109
  // reverse extraction `screen-extraction`. Read from frontmatter, default
110
110
  // forward; only these two are recognised (others pass through, flagged).
111
111
  const source = screenSource(fm.source, inputs.findings, screenId);
@@ -130,7 +130,7 @@ export async function buildScreens(inputs) {
130
130
  evidence_class: evidenceClass(fm),
131
131
  source_sdd_id: "",
132
132
  });
133
- // Evidence integrity (ADR 0168 §4.2, defect f): a reverse-extracted screen
133
+ // Evidence integrity (M-ADR 0168 §4.2, defect f): a reverse-extracted screen
134
134
  // must cite its route + component evidence with a `[^…]` footnote. Forward-
135
135
  // authored screens make no such claim, so the check is reverse-scoped.
136
136
  if (source === "screen-extraction" && !/\[\^[^\]]+\]/.test(text)) {
@@ -142,7 +142,7 @@ export async function buildScreens(inputs) {
142
142
  }
143
143
  else {
144
144
  // Strip inline `#` comments before any scan so a `Capitalised(` token in a
145
- // comment can't emit a phantom component (ADR 0168 §4.4, defect g).
145
+ // comment can't emit a phantom component (M-ADR 0168 §4.4, defect g).
146
146
  const clean = stripBlockComments(block);
147
147
  // Component invocations: Capitalised call heads (e.g. Stack(, StatCard().
148
148
  // Local id references (lowercase, no parens) and list literals are skipped.
@@ -168,12 +168,12 @@ export async function buildScreens(inputs) {
168
168
  const aKey = `${screenId}__${screenSlug(action)}`;
169
169
  if (!screenAction.has(aKey)) {
170
170
  // event_id left empty: external-capable, resolved against the event
171
- // catalogue when an explicit mapping exists (best-effort per ADR 0166).
171
+ // catalogue when an explicit mapping exists (best-effort per M-ADR 0166).
172
172
  screenAction.set(aKey, { id: aKey, screen_id: screenId, action, event_id: "", source_sdd_id: "" });
173
173
  }
174
174
  }
175
- // Composition-depth validation (ADR 0168): structural fidelity floor +
176
- // status gate + the archetype-aware root check (ADR 0169). The vocabulary-
175
+ // Composition-depth validation (M-ADR 0168): structural fidelity floor +
176
+ // status gate + the archetype-aware root check (M-ADR 0169). The vocabulary-
177
177
  // free checks reason about tree shape + status only; root-is-leaf consults
178
178
  // the archetype map when the screen declares a `platform:`.
179
179
  const platform = typeof fm.platform === "string" ? fm.platform : undefined;
@@ -192,7 +192,7 @@ export async function buildScreens(inputs) {
192
192
  if (!fid) {
193
193
  // App/integration screens can realise a platform-resident feature
194
194
  // (MFE-over-platform: the platform owns the capability tree and its
195
- // features — ADR 0177 single-slug). Emit a deferred cross-SDD edge
195
+ // features — M-ADR 0177 single-slug). Emit a deferred cross-SDD edge
196
196
  // carrying the bare realises slug; the aggregator resolves it to the
197
197
  // platform feature id, and the FK validator logs it `[finding] external`
198
198
  // for this build (screen_feature.feature_id is external_for_profiles).
@@ -261,24 +261,24 @@ function emitDepthFindings(findings, screenId, status, m, ctx) {
261
261
  findings.add("coverage", `screen ${screenId}: composition is flat — root has ${m.rootChildCount} children at depth ${m.maxDepth} (screen-flat-composition)`);
262
262
  }
263
263
  // Status gate (keystone): `working` is the only maturity that asserts a
264
- // faithful, complete composition (ADR 0168 §4.3). provisional/unknown/absent
264
+ // faithful, complete composition (M-ADR 0168 §4.3). provisional/unknown/absent
265
265
  // make no completeness claim and are not gated.
266
266
  if (status === COMPLETENESS_STATUS && (empty || flat)) {
267
267
  findings.add("convention", `screen ${screenId}: status "${COMPLETENESS_STATUS}" exceeds composition depth — block is ${empty ? "empty" : "flat"} (screen-status-exceeds-depth)`, "error");
268
268
  }
269
- // Mis-rooted (ADR 0168 §4.2 screen-root-is-leaf, archetype-aware per ADR 0169):
269
+ // Mis-rooted (M-ADR 0168 §4.2 screen-root-is-leaf, archetype-aware per M-ADR 0169):
270
270
  // root is a leaf/control rather than a screen container. isContainer combines
271
271
  // the appears-with-children heuristic (containers) with the archetype map.
272
272
  // Only fires when the screen declares a known `platform:` — without a map the
273
273
  // heuristic alone can't distinguish an empty container root (Stack([])) from a
274
- // genuine leaf root (Button([])), so the check no-ops (ADR 0168 §4.5).
274
+ // genuine leaf root (Button([])), so the check no-ops (M-ADR 0168 §4.5).
275
275
  if (m.rootComponent !== "" && m.rootChildCount === 0) {
276
276
  const byMap = isContainerArchetype(m.rootComponent, platform, overlay);
277
277
  if (byMap === false && !m.containers.has(m.rootComponent)) {
278
278
  findings.add("convention", `screen ${screenId}: root component "${m.rootComponent}" is a leaf, not a container (screen-root-is-leaf)`, "error");
279
279
  }
280
280
  }
281
- // Styling-primitive leakage (ADR 0168 §4.2 screen-styling-primitive-as-structure):
281
+ // Styling-primitive leakage (M-ADR 0168 §4.2 screen-styling-primitive-as-structure):
282
282
  // an adopter-flagged decorative primitive composed as a structural component.
283
283
  // No-ops when the adopter declares no decorative set.
284
284
  if (decorativePrimitives !== undefined && decorativePrimitives.size > 0) {
@@ -317,7 +317,7 @@ function screenSlug(s) {
317
317
  function normalise(s) {
318
318
  return s.trim().toLowerCase().replace(/\s+/g, " ");
319
319
  }
320
- // Recognised direction discriminators (ADR 0135). Others pass through flagged.
320
+ // Recognised direction discriminators (M-ADR 0135). Others pass through flagged.
321
321
  const SCREEN_SOURCES = new Set(["screen-spec", "screen-extraction"]);
322
322
  function screenSource(value, findings, screenId) {
323
323
  if (typeof value !== "string" || value.trim() === "")
@@ -22,7 +22,7 @@
22
22
  // the platform-root SDD validates internally first per the fk-validator
23
23
  // optimistic-resolve rule.
24
24
  //
25
- // Pattern documented in sdd-method ADR 0147.
25
+ // Pattern documented in sdd-method M-ADR 0147.
26
26
  import { readFile, readdir } from "node:fs/promises";
27
27
  import { join } from "node:path";
28
28
  const REPOS_DIR_REL = "repos";
@@ -1,8 +1,8 @@
1
1
  // Primary reverse-method walker for the service_entity edge catalogue per
2
- // ADR 0159 §4.5. Reads per-domain `## Data Store Mapping` markdown tables in
2
+ // M-ADR 0159 §4.5. Reads per-domain `## Data Store Mapping` markdown tables in
3
3
  // `docs/domains/{domain}/architecture/data-model.mermaid.md` files.
4
4
  //
5
- // Prototype findings (per ADR 0159 — data-store-mapping table variations):
5
+ // Prototype findings (per M-ADR 0159 — data-store-mapping table variations):
6
6
  // - Two heading variants: `## Data Store Mapping` and `## Data Store Inventory`
7
7
  // (the latter is store-centric — different schema, no Entity column — and
8
8
  // gets skipped with a convention finding).
@@ -14,7 +14,7 @@
14
14
  // - Cross-domain entity references in tables resolve against entities.csv
15
15
  // from any domain (not just the table's own domain) — same-domain first.
16
16
  //
17
- // Service-name resolution rules (ADR 0159 §4.7):
17
+ // Service-name resolution rules (M-ADR 0159 §4.7):
18
18
  // 1. Try alias-map exact match (case-insensitive on display name).
19
19
  // 2. Try entitySlug(display_name) ⋈ entitySlug(services.name).
20
20
  // 3. Try entitySlug(display_name) ⋈ last_segment(services.id).