@sdd-method/author-cli 0.0.0-stage → 0.1.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 (203) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +17 -0
  3. package/README.md +6 -2
  4. package/dist/author-bin.js +8 -0
  5. package/dist/author-program.js +99 -0
  6. package/dist/lib/adr-origin.js +59 -0
  7. package/dist/lib/agent-harness.js +37 -0
  8. package/dist/lib/api-surface/__fixtures__/serverless-repo/serverless/serverless.yml +18 -0
  9. package/dist/lib/api-surface/analyzers/library.js +95 -0
  10. package/dist/lib/api-surface/analyzers/serverless.js +336 -0
  11. package/dist/lib/api-surface/analyzers/spring.js +237 -0
  12. package/dist/lib/api-surface/analyzers/terraform.js +109 -0
  13. package/dist/lib/api-surface/detect.js +25 -0
  14. package/dist/lib/api-surface/emit.js +109 -0
  15. package/dist/lib/api-surface/file-set.js +43 -0
  16. package/dist/lib/api-surface/index.js +134 -0
  17. package/dist/lib/api-surface/types.js +17 -0
  18. package/dist/lib/briefing/index.js +13 -0
  19. package/dist/lib/briefing/render.js +132 -0
  20. package/dist/lib/briefing/variables.js +92 -0
  21. package/dist/lib/catalogue/application-archetype/vocabulary.js +177 -0
  22. package/dist/lib/catalogue/archetype/maps/android.yaml +66 -0
  23. package/dist/lib/catalogue/archetype/maps/swiftui.yaml +58 -0
  24. package/dist/lib/catalogue/archetype/maps/uikit.yaml +48 -0
  25. package/dist/lib/catalogue/archetype/maps/web-generic.yaml +86 -0
  26. package/dist/lib/catalogue/archetype/openui-archetype-vocabulary.yaml +46 -0
  27. package/dist/lib/catalogue/archetype/resolver.js +103 -0
  28. package/dist/lib/catalogue/archetype/wireframe.js +418 -0
  29. package/dist/lib/catalogue/baseline-check.js +151 -0
  30. package/dist/lib/catalogue/build.js +1502 -0
  31. package/dist/lib/catalogue/builders/adr-supersedes.js +103 -0
  32. package/dist/lib/catalogue/builders/adrs.js +222 -0
  33. package/dist/lib/catalogue/builders/application-conformance-coverage.js +47 -0
  34. package/dist/lib/catalogue/builders/application-data-plane-bindings.js +115 -0
  35. package/dist/lib/catalogue/builders/c4-components.js +229 -0
  36. package/dist/lib/catalogue/builders/c4-containers.js +240 -0
  37. package/dist/lib/catalogue/builders/c4-entity-rollups.js +87 -0
  38. package/dist/lib/catalogue/builders/c4-external-systems.js +194 -0
  39. package/dist/lib/catalogue/builders/c4-l1-candidates.js +107 -0
  40. package/dist/lib/catalogue/builders/c4-systems.js +98 -0
  41. package/dist/lib/catalogue/builders/capabilities.js +488 -0
  42. package/dist/lib/catalogue/builders/capability-domain-model.js +74 -0
  43. package/dist/lib/catalogue/builders/capability-edges.js +256 -0
  44. package/dist/lib/catalogue/builders/capability-spec-walker.js +99 -0
  45. package/dist/lib/catalogue/builders/capability-spec.js +348 -0
  46. package/dist/lib/catalogue/builders/container-dependency.js +183 -0
  47. package/dist/lib/catalogue/builders/contracts.js +402 -0
  48. package/dist/lib/catalogue/builders/data-items.js +344 -0
  49. package/dist/lib/catalogue/builders/data-store-mapping.js +113 -0
  50. package/dist/lib/catalogue/builders/domain-models.js +175 -0
  51. package/dist/lib/catalogue/builders/domains.js +119 -0
  52. package/dist/lib/catalogue/builders/edge-label-merge.js +19 -0
  53. package/dist/lib/catalogue/builders/entities-manifest.js +160 -0
  54. package/dist/lib/catalogue/builders/entities-mermaid.js +189 -0
  55. package/dist/lib/catalogue/builders/entity-persistence.js +102 -0
  56. package/dist/lib/catalogue/builders/entity-slug.js +47 -0
  57. package/dist/lib/catalogue/builders/event-catalog-walker.js +186 -0
  58. package/dist/lib/catalogue/builders/event-edges.js +327 -0
  59. package/dist/lib/catalogue/builders/events.js +168 -0
  60. package/dist/lib/catalogue/builders/evidence-class.js +43 -0
  61. package/dist/lib/catalogue/builders/evidence-target-repo.js +67 -0
  62. package/dist/lib/catalogue/builders/external-system-exemption.js +120 -0
  63. package/dist/lib/catalogue/builders/feature-contract.js +77 -0
  64. package/dist/lib/catalogue/builders/feature-domain-model.js +50 -0
  65. package/dist/lib/catalogue/builders/feature-grounding-walker.js +254 -0
  66. package/dist/lib/catalogue/builders/feature-grounding.js +391 -0
  67. package/dist/lib/catalogue/builders/feature-groups.js +63 -0
  68. package/dist/lib/catalogue/builders/feature-subscription-channels.js +228 -0
  69. package/dist/lib/catalogue/builders/feature-tree-walker.js +295 -0
  70. package/dist/lib/catalogue/builders/features.js +300 -0
  71. package/dist/lib/catalogue/builders/glossary-xref.js +378 -0
  72. package/dist/lib/catalogue/builders/glossary.js +204 -0
  73. package/dist/lib/catalogue/builders/implementations.js +214 -0
  74. package/dist/lib/catalogue/builders/index.js +64 -0
  75. package/dist/lib/catalogue/builders/integration-components.js +344 -0
  76. package/dist/lib/catalogue/builders/integrations.js +181 -0
  77. package/dist/lib/catalogue/builders/persona-c4system.js +149 -0
  78. package/dist/lib/catalogue/builders/persona-container.js +133 -0
  79. package/dist/lib/catalogue/builders/persona-journey-edges.js +133 -0
  80. package/dist/lib/catalogue/builders/personas.js +152 -0
  81. package/dist/lib/catalogue/builders/product-edges.js +106 -0
  82. package/dist/lib/catalogue/builders/product-platform-dependencies.js +405 -0
  83. package/dist/lib/catalogue/builders/products.js +162 -0
  84. package/dist/lib/catalogue/builders/quality-scores.js +635 -0
  85. package/dist/lib/catalogue/builders/repo-capability-exemption.js +61 -0
  86. package/dist/lib/catalogue/builders/repo-dependency.js +127 -0
  87. package/dist/lib/catalogue/builders/repo-edges.js +340 -0
  88. package/dist/lib/catalogue/builders/repositories.js +458 -0
  89. package/dist/lib/catalogue/builders/scaffolds.js +197 -0
  90. package/dist/lib/catalogue/builders/screen-tree.js +222 -0
  91. package/dist/lib/catalogue/builders/screens.js +396 -0
  92. package/dist/lib/catalogue/builders/service-capability-exemption.js +63 -0
  93. package/dist/lib/catalogue/builders/service-contract-consumes.js +186 -0
  94. package/dist/lib/catalogue/builders/service-contract.js +74 -0
  95. package/dist/lib/catalogue/builders/service-entity-tables.js +187 -0
  96. package/dist/lib/catalogue/builders/services.js +232 -0
  97. package/dist/lib/catalogue/builders/system-external-dependency.js +115 -0
  98. package/dist/lib/catalogue/builders/user-journeys.js +253 -0
  99. package/dist/lib/catalogue/canonical-schema.yaml +4117 -0
  100. package/dist/lib/catalogue/cli-version.js +10 -0
  101. package/dist/lib/catalogue/contract-id.js +49 -0
  102. package/dist/lib/catalogue/coverage-predicates.js +603 -0
  103. package/dist/lib/catalogue/coverage-render.js +552 -0
  104. package/dist/lib/catalogue/coverage-run.js +115 -0
  105. package/dist/lib/catalogue/csv-reader.js +97 -0
  106. package/dist/lib/catalogue/csv-writer.js +96 -0
  107. package/dist/lib/catalogue/data-plane-coherence.js +77 -0
  108. package/dist/lib/catalogue/ddl-generator.js +233 -0
  109. package/dist/lib/catalogue/dedupe-keyed-tables.js +0 -0
  110. package/dist/lib/catalogue/engine-validator.js +239 -0
  111. package/dist/lib/catalogue/ext-id.js +76 -0
  112. package/dist/lib/catalogue/feature-grounding-aliases-loader.js +65 -0
  113. package/dist/lib/catalogue/feature-repo-zero-resolution-summary.js +31 -0
  114. package/dist/lib/catalogue/finding-identity.js +183 -0
  115. package/dist/lib/catalogue/findings.js +46 -0
  116. package/dist/lib/catalogue/fk-validator.js +136 -0
  117. package/dist/lib/catalogue/freshness.js +70 -0
  118. package/dist/lib/catalogue/gate-checks.js +338 -0
  119. package/dist/lib/catalogue/graph-projection.js +346 -0
  120. package/dist/lib/catalogue/impact-run.js +228 -0
  121. package/dist/lib/catalogue/manifest-loader.js +435 -0
  122. package/dist/lib/catalogue/platform-scope.js +7 -0
  123. package/dist/lib/catalogue/population-checks.js +87 -0
  124. package/dist/lib/catalogue/relationship-semantics.js +66 -0
  125. package/dist/lib/catalogue/reverse-product-quality.js +99 -0
  126. package/dist/lib/catalogue/rubric-coverage-map.yaml +871 -0
  127. package/dist/lib/catalogue/runway-vocabulary-loader.js +56 -0
  128. package/dist/lib/catalogue/scaffold-delta.js +261 -0
  129. package/dist/lib/catalogue/schema-compat.js +52 -0
  130. package/dist/lib/catalogue/schema-loader.js +87 -0
  131. package/dist/lib/catalogue/scores-coverage.js +573 -0
  132. package/dist/lib/catalogue/screen-validation-loader.js +89 -0
  133. package/dist/lib/catalogue/service-architecture-parser.js +193 -0
  134. package/dist/lib/catalogue/service-contract-aliases-loader.js +109 -0
  135. package/dist/lib/catalogue/staleness-verdicts.js +345 -0
  136. package/dist/lib/catalogue/traceability-loader.js +350 -0
  137. package/dist/lib/catalogue/validate.js +200 -0
  138. package/dist/lib/catalogue/version.js +18 -0
  139. package/dist/lib/conformance/binding.js +70 -0
  140. package/dist/lib/conformance/finding.js +69 -0
  141. package/dist/lib/conformance/index.js +578 -0
  142. package/dist/lib/context-pull/index.js +191 -0
  143. package/dist/lib/context-pull/resolve.js +70 -0
  144. package/dist/lib/context-pull/retired-mcp-setup.js +7 -0
  145. package/dist/lib/contracts-availability/index.js +267 -0
  146. package/dist/lib/generate-briefing/index.js +77 -0
  147. package/dist/lib/list/discover.js +100 -0
  148. package/dist/lib/list/format.js +84 -0
  149. package/dist/lib/list/index.js +16 -0
  150. package/dist/lib/lock-file.js +437 -0
  151. package/dist/lib/pack/log.js +18 -0
  152. package/dist/lib/pack/schema.js +121 -0
  153. package/dist/lib/pack/seed.js +182 -0
  154. package/dist/lib/pack/validate.js +49 -0
  155. package/dist/lib/package-version.js +92 -0
  156. package/dist/lib/plan/index.js +69 -0
  157. package/dist/lib/plan/read-sdd.js +102 -0
  158. package/dist/lib/plan/render-template.js +84 -0
  159. package/dist/lib/repo-kind.js +129 -0
  160. package/dist/lib/scaffold/domain.js +234 -0
  161. package/dist/lib/scaffold/registry.js +136 -0
  162. package/dist/lib/scripts.js +7 -0
  163. package/dist/lib/sdd-index/repo-config.js +245 -0
  164. package/dist/lib/sdd-reader.js +91 -0
  165. package/dist/lib/snapshot/advance.js +454 -0
  166. package/dist/lib/snapshot/diff.js +190 -0
  167. package/dist/lib/snapshot/fold.js +208 -0
  168. package/dist/lib/snapshot/land.js +178 -0
  169. package/dist/lib/snapshot/manifest.js +206 -0
  170. package/dist/lib/snapshot/targets.js +169 -0
  171. package/dist/lib/snapshot/verify.js +91 -0
  172. package/dist/lib/sync/briefing-merge.js +106 -0
  173. package/dist/lib/sync/bundle-cache.js +109 -0
  174. package/dist/lib/sync/yaml-block-merge.js +120 -0
  175. package/dist/lib/validate/aggregate.js +51 -0
  176. package/dist/lib/validate/bundle-coherence.js +74 -0
  177. package/dist/lib/validate/dispatch.js +243 -0
  178. package/dist/lib/validate/index.js +110 -0
  179. package/dist/lib/validate/plan.js +54 -0
  180. package/dist/lib/validate/runner.js +85 -0
  181. package/dist/lib/validate/yaml-parse.js +112 -0
  182. package/dist/maintain-version.json +4 -0
  183. package/dist/verbs/api-surface.js +71 -0
  184. package/dist/verbs/catalogue-check.js +28 -0
  185. package/dist/verbs/catalogue-verify.js +114 -0
  186. package/dist/verbs/conformance.js +34 -0
  187. package/dist/verbs/context.js +39 -0
  188. package/dist/verbs/contracts.js +34 -0
  189. package/dist/verbs/generate-briefing.js +29 -0
  190. package/dist/verbs/list.js +45 -0
  191. package/dist/verbs/pack-validate.js +17 -0
  192. package/dist/verbs/path-option.js +25 -0
  193. package/dist/verbs/plan.js +42 -0
  194. package/dist/verbs/render.js +87 -0
  195. package/dist/verbs/scaffold.js +83 -0
  196. package/dist/verbs/snapshot.js +134 -0
  197. package/dist/verbs/validate.js +48 -0
  198. package/dist/verify-program.js +122 -0
  199. package/dist/verify-version.json +4 -0
  200. package/package.json +32 -4
  201. package/scripts/validate-app-dependencies.sh +664 -0
  202. package/scripts/validate-app-docs.sh +199 -0
  203. package/scripts/validate-integration-docs.sh +505 -0
@@ -0,0 +1,43 @@
1
+ // File-level evidence-class extraction for data-model markdown, per the
2
+ // documentation-reverse anchor convention (ADR 0175 / sdd-method
3
+ // `docs/method/methods/reverse/documentation-reverse-anchor-schema.md`).
4
+ //
5
+ // Documentation-reversed data models open with a fenced ```yaml block near
6
+ // the top of the file carrying the anchor metadata:
7
+ //
8
+ // ```yaml
9
+ // evidence-class: vendor-doc
10
+ // vendor: workos
11
+ // vendor-version: workos-docs@2026-06-10
12
+ // anchors: [...]
13
+ // ```
14
+ //
15
+ // The extractor finds the FIRST ```yaml fence in the markdown, YAML-parses
16
+ // the fence body, and reads `evidence-class` (alias `evidence_class`).
17
+ // Everything is best-effort: no fence, a non-mapping body, an unparseable
18
+ // body, or a missing key all yield "" — code-reversed data models carry no
19
+ // fence and stay empty.
20
+ import { parse as parseYaml } from "yaml";
21
+ const RE_YAML_FENCE = /^[ \t]*```[ \t]*ya?ml[ \t]*\r?\n([\s\S]*?)\r?\n[ \t]*```[ \t]*$/im;
22
+ /**
23
+ * Extract the file-level `evidence-class:` declaration from a markdown
24
+ * document's leading ```yaml fence. Returns "" when absent or malformed.
25
+ */
26
+ export function extractFileEvidenceClass(markdown) {
27
+ const m = RE_YAML_FENCE.exec(markdown);
28
+ if (!m)
29
+ return "";
30
+ let parsed;
31
+ try {
32
+ parsed = parseYaml(m[1] ?? "");
33
+ }
34
+ catch {
35
+ return "";
36
+ }
37
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
38
+ return "";
39
+ const obj = parsed;
40
+ const value = obj["evidence-class"] ?? obj["evidence_class"];
41
+ return typeof value === "string" ? value.trim() : "";
42
+ }
43
+ //# sourceMappingURL=evidence-class.js.map
@@ -0,0 +1,67 @@
1
+ // evidence_target_repo (M-ADR 0214 §4.10): a reverse evidence YAML names, through a top-level
2
+ // `target_repo` (one id or a list), repositories ANOTHER SDD owns. A reference, never a claim — the
3
+ // repository is described by exactly one SDD, elsewhere — so the FK is external at build and must
4
+ // resolve at aggregation. Naming a repository this SDD itself describes is reported: that is the
5
+ // SDD's own, and its reverse-metadata already says so.
6
+ import { readdir, readFile } from "node:fs/promises";
7
+ import { join, relative, sep } from "node:path";
8
+ import { parse as parseYaml } from "yaml";
9
+ const SKIP = new Set(["node_modules", ".git", "source", "catalogues", "catalogues-aggregated", "dist", "bundle-cache"]);
10
+ async function evidenceFiles(root, current, inEvidence, out) {
11
+ let entries;
12
+ try {
13
+ entries = await readdir(current, { withFileTypes: true });
14
+ }
15
+ catch {
16
+ return;
17
+ }
18
+ for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
19
+ if (SKIP.has(e.name))
20
+ continue;
21
+ const abs = join(current, e.name);
22
+ if (e.isDirectory()) {
23
+ if (relative(root, abs).split(sep).length > 10)
24
+ continue;
25
+ await evidenceFiles(root, abs, inEvidence || e.name === "reverse-evidence", out);
26
+ }
27
+ else if (inEvidence && e.isFile() && /\.ya?ml$/.test(e.name)) {
28
+ out.push(abs);
29
+ }
30
+ }
31
+ }
32
+ export async function buildEvidenceTargetRepos(sddPath, ownRepoIds, findings) {
33
+ const files = [];
34
+ await evidenceFiles(sddPath, sddPath, false, files);
35
+ const rows = new Map();
36
+ for (const abs of files) {
37
+ const rel = relative(sddPath, abs);
38
+ let doc;
39
+ try {
40
+ doc = parseYaml(await readFile(abs, "utf-8"));
41
+ }
42
+ catch {
43
+ continue; // an unparseable file is the yaml-parse validator's finding, not this one's
44
+ }
45
+ if (!doc || typeof doc !== "object" || Array.isArray(doc))
46
+ continue;
47
+ const raw = doc["target_repo"];
48
+ if (raw === undefined)
49
+ continue;
50
+ const ids = Array.isArray(raw) ? raw : [raw];
51
+ for (const v of ids) {
52
+ if (typeof v !== "string" || v.trim() === "") {
53
+ findings.add("convention", `${rel}: target_repo must be a repository id or a list of them — ${JSON.stringify(v)} skipped`);
54
+ continue;
55
+ }
56
+ const repoId = v.trim();
57
+ if (ownRepoIds.has(repoId)) {
58
+ findings.add("convention", `${rel}: target_repo ${repoId} is a repository this SDD describes — it is yours, not a reference to another SDD's (M-ADR 0214 §4.10); drop it`);
59
+ continue;
60
+ }
61
+ const id = `${rel}::${repoId}`;
62
+ rows.set(id, { id, evidence_path: rel, repo_id: repoId, source_sdd_id: "" });
63
+ }
64
+ }
65
+ return [...rows.values()];
66
+ }
67
+ //# sourceMappingURL=evidence-target-repo.js.map
@@ -0,0 +1,120 @@
1
+ // external_system_exemption builder — M-ADR 0189 §4.6 (gov WS15).
2
+ //
3
+ // Emits one row per entry in `orchestration/external-system-exemptions.yaml`:
4
+ //
5
+ // exemptions:
6
+ // - external_system_id: "ext-acme-email"
7
+ // reason: "third-party-saas" # closed vocabulary, canonical-schema.yaml
8
+ // note: "Transactional email vendor; SMTP relay only."
9
+ //
10
+ // An adjudicator can prove a candidate INTERNAL; it cannot prove one
11
+ // EXTERNAL — externality is an assertion the author is accountable for
12
+ // (the adopter owns assertions; the method owns inferences). Each row
13
+ // retires exactly one "kept external, unverified" coverage finding at
14
+ // aggregation, and nothing else.
15
+ //
16
+ // VALIDATION ORDER IS LOAD-BEARING, per the M-ADR 0188 §4.3 precedent:
17
+ // every rejection below drops the exemption row and leaves the candidate
18
+ // unverified — reported by the aggregator's coverage predicate. It never
19
+ // drops the candidate. An exemption that fails validation must not quietly
20
+ // succeed at the one thing an exemption does, which is remove a candidate
21
+ // from the invariant's attention.
22
+ import { readFile } from "node:fs/promises";
23
+ import { join } from "node:path";
24
+ import { parse as parseYaml } from "yaml";
25
+ export const EXTERNAL_EXEMPTIONS_REL_PATH = "orchestration/external-system-exemptions.yaml";
26
+ export async function buildExternalSystemExemption(inputs) {
27
+ const abs = join(inputs.sddPath, EXTERNAL_EXEMPTIONS_REL_PATH);
28
+ let raw;
29
+ try {
30
+ raw = await readFile(abs, "utf-8");
31
+ }
32
+ catch (err) {
33
+ if (err.code === "ENOENT")
34
+ return [];
35
+ throw err;
36
+ }
37
+ const doc = parseYaml(raw);
38
+ const entries = Array.isArray(doc?.exemptions) ? doc.exemptions : [];
39
+ const rows = [];
40
+ // Resolved id → the id it was AUTHORED as. A Map rather than a Set because a
41
+ // collision has to name both sources, and after cut-window re-keying the two
42
+ // colliding entries may have been authored under different ids — reporting
43
+ // only the survivor would name neither of the things the author wrote.
44
+ const seen = new Map();
45
+ for (const item of entries) {
46
+ if (!item || typeof item !== "object")
47
+ continue;
48
+ const o = item;
49
+ let extId = typeof o["external_system_id"] === "string" ? o["external_system_id"].trim() : "";
50
+ // Captured before cut-window re-keying rewrites `extId`, so a collision can
51
+ // report what the author actually wrote rather than what it resolved to.
52
+ const authoredId = extId;
53
+ const reason = typeof o["reason"] === "string" ? o["reason"].trim() : "";
54
+ const note = typeof o["note"] === "string" ? o["note"].trim() : "";
55
+ if (extId === "")
56
+ continue;
57
+ if (!inputs.knownExternalIds.has(extId)) {
58
+ // M-ADR 0203 §4.2 cut window: an id the 2.x builder minted from the
59
+ // display name is provably tool-authored — regenerable, not drift.
60
+ const mapped = inputs.legacyIdMap?.get(extId);
61
+ if (mapped !== undefined && mapped.length === 1) {
62
+ inputs.findings.add("inferred", `${EXTERNAL_EXEMPTIONS_REL_PATH}: exemption for "${extId}" re-keyed to "${mapped[0]}" — ` +
63
+ `2.x name-slug id migrated per the 3.0.0 ext-id map (regenerable, not drift); update the yaml to retire this finding`);
64
+ extId = mapped[0];
65
+ }
66
+ else if (mapped !== undefined && mapped.length > 1) {
67
+ inputs.findings.add("convention", `${EXTERNAL_EXEMPTIONS_REL_PATH}: exemption for "${extId}" — this 2.x name-slug id maps to ` +
68
+ `${mapped.length} declared identities at 3.0.0 [${mapped.join(", ")}]; the author must choose one; exemption dropped`);
69
+ continue;
70
+ }
71
+ else {
72
+ inputs.findings.add("convention", `${EXTERNAL_EXEMPTIONS_REL_PATH}: exemption for "${extId}" — this SDD declares no such external-system candidate ` +
73
+ `(3.0.0 ids derive from diagram entry ids; \`sdd-cli catalogue migration-map\` lists the 2.x→3.0.0 id map); exemption dropped`);
74
+ continue;
75
+ }
76
+ }
77
+ if (reason === "") {
78
+ inputs.findings.add("convention", `${EXTERNAL_EXEMPTIONS_REL_PATH}: exemption for "${extId}" declares no reason — dropped, candidate stays unverified (M-ADR 0189 §4.6)`);
79
+ continue;
80
+ }
81
+ if (!inputs.allowedReasons.has(reason)) {
82
+ const allowed = [...inputs.allowedReasons].sort().join(", ");
83
+ inputs.findings.add("convention", `${EXTERNAL_EXEMPTIONS_REL_PATH}: exemption for "${extId}" reason "${reason}" is outside the closed vocabulary [${allowed}] — dropped, candidate stays unverified (M-ADR 0189 §4.6)`);
84
+ continue;
85
+ }
86
+ if (note === "") {
87
+ inputs.findings.add("convention", `${EXTERNAL_EXEMPTIONS_REL_PATH}: exemption for "${extId}" has no note — the reason makes exemptions countable, the note makes them reviewable; dropped (M-ADR 0189 §4.6)`);
88
+ continue;
89
+ }
90
+ // A collapse is a FINDING, not a silent drop. Two authored exemptions
91
+ // resolving to one candidate means one author's reason and note are
92
+ // discarded, and an exemption's whole job is to carry a reviewable
93
+ // justification — so dropping one silently retires a candidate from the
94
+ // invariant's attention under a justification nobody chose. Both sibling
95
+ // builders (repo-capability-exemption, service-capability-exemption) already
96
+ // report their duplicates as `divergence`; this one was the outlier.
97
+ //
98
+ // Names BOTH SOURCES, which is more than "duplicate id" would say: after
99
+ // cut-window re-keying the two entries can have been authored under
100
+ // DIFFERENT ids, so the collision is invisible in the yaml — the author sees
101
+ // two unlike entries, and only the resolved form collides. Reporting the
102
+ // resolved id alone would name something they never wrote.
103
+ const firstAuthoredAs = seen.get(extId);
104
+ if (firstAuthoredAs !== undefined) {
105
+ const bothAuthoredSame = firstAuthoredAs === authoredId;
106
+ inputs.findings.add("divergence", bothAuthoredSame
107
+ ? `${EXTERNAL_EXEMPTIONS_REL_PATH}: duplicate external_system_exemption for "${extId}" — first declaration wins; ` +
108
+ `the later entry's reason "${reason}" and note are discarded. One candidate takes one exemption; merge them.`
109
+ : `${EXTERNAL_EXEMPTIONS_REL_PATH}: exemptions authored as "${firstAuthoredAs}" and "${authoredId}" both resolve to ` +
110
+ `"${extId}" — first declaration wins; the later entry's reason "${reason}" and note are discarded. ` +
111
+ `Two authored entries cannot both exempt one candidate: merge them, or correct whichever id is wrong.`);
112
+ continue;
113
+ }
114
+ seen.set(extId, authoredId);
115
+ rows.push({ id: extId, external_system_id: extId, reason, note });
116
+ }
117
+ rows.sort((a, b) => String(a["id"]).localeCompare(String(b["id"])));
118
+ return rows;
119
+ }
120
+ //# sourceMappingURL=external-system-exemption.js.map
@@ -0,0 +1,77 @@
1
+ // Edge builder for feature_contract — derived from two join paths.
2
+ //
3
+ // Forward: capability_feature ⋈ capability_contract (on capability_id)
4
+ // Reverse: feature_service ⋈ service_contract (on service_id)
5
+ //
6
+ // Both paths are run and results are merged; duplicate (feature_id, contract_id)
7
+ // pairs are deduplicated with a `[finding] inferred` log line recording that
8
+ // both paths produced the same edge.
9
+ //
10
+ // Added in schema 2.3.0 — see catalogue-metamodel.md §"Edge catalogue status"
11
+ // and the companion plan
12
+ // `content/plans/document-non-catalogued-artefacts-and-add-feature-contract.md`.
13
+ export function buildFeatureContract(inputs) {
14
+ const seen = new Map();
15
+ // Path 1 (forward): capability_feature ⋈ capability_contract on capability_id.
16
+ const contractsByCap = new Map();
17
+ for (const row of inputs.capabilityContractRows) {
18
+ const capId = String(row["capability_id"] ?? "");
19
+ if (capId === "")
20
+ continue;
21
+ const contractId = String(row["contract_id"] ?? "");
22
+ if (contractId === "")
23
+ continue;
24
+ const arr = contractsByCap.get(capId) ?? [];
25
+ arr.push(contractId);
26
+ contractsByCap.set(capId, arr);
27
+ }
28
+ for (const row of inputs.capabilityFeatureRows) {
29
+ const featureId = String(row["feature_id"] ?? "");
30
+ const capId = String(row["capability_id"] ?? "");
31
+ if (featureId === "" || capId === "")
32
+ continue;
33
+ for (const contractId of contractsByCap.get(capId) ?? []) {
34
+ emit(seen, featureId, contractId, "cap-feature-cap-contract", inputs.findings);
35
+ }
36
+ }
37
+ // Path 2 (reverse): feature_service ⋈ service_contract on service_id.
38
+ // service_contract may carry rows with role=exposes|consumes; both
39
+ // count as the service being wired to the contract for traceability.
40
+ const contractsByService = new Map();
41
+ for (const row of inputs.serviceContractRows) {
42
+ const svcId = String(row["service_id"] ?? "");
43
+ if (svcId === "")
44
+ continue;
45
+ const contractId = String(row["contract_id"] ?? "");
46
+ if (contractId === "")
47
+ continue;
48
+ const arr = contractsByService.get(svcId) ?? [];
49
+ arr.push(contractId);
50
+ contractsByService.set(svcId, arr);
51
+ }
52
+ for (const row of inputs.featureServiceRows) {
53
+ const featureId = String(row["feature_id"] ?? "");
54
+ const svcId = String(row["service_id"] ?? "");
55
+ if (featureId === "" || svcId === "")
56
+ continue;
57
+ for (const contractId of contractsByService.get(svcId) ?? []) {
58
+ emit(seen, featureId, contractId, "feature-service-service-contract", inputs.findings);
59
+ }
60
+ }
61
+ // Sort by id for deterministic output (same convention as capability-edges.ts).
62
+ return Array.from(seen.values()).sort((a, b) => String(a["id"]).localeCompare(String(b["id"])));
63
+ }
64
+ function emit(seen, featureId, contractId, derivationPath, findings) {
65
+ const id = `${featureId}__${contractId}`;
66
+ if (seen.has(id)) {
67
+ findings.add("inferred", `feature_contract derived by both join paths: ${id} (kept first; would also derive via ${derivationPath})`);
68
+ return;
69
+ }
70
+ seen.set(id, {
71
+ id,
72
+ feature_id: featureId,
73
+ contract_id: contractId,
74
+ derivation_path: derivationPath,
75
+ });
76
+ }
77
+ //# sourceMappingURL=feature-contract.js.map
@@ -0,0 +1,50 @@
1
+ // Edge builder for feature_domain_model — emits `feature_domain_model.csv`.
2
+ //
3
+ // Platform-profile only. Non-platform SDDs return an empty list.
4
+ //
5
+ // Derived from capability_feature ⋈ capability_domain_model on
6
+ // capability_id. Orphan features (no capability link) get no row;
7
+ // the join produces nothing for them.
8
+ //
9
+ // Output is deduplicated on (feature_id, domain_model_id). If a feature
10
+ // links to multiple capabilities that each link to a different domain
11
+ // model, both pairs are emitted. If multiple capabilities link to the
12
+ // SAME domain model, the duplicate is silently dropped.
13
+ //
14
+ // Added in schema 2.4.0 per ADR 0142.
15
+ export function buildFeatureDomainModel(inputs) {
16
+ if (inputs.manifest.profile !== "platform-profile") {
17
+ return [];
18
+ }
19
+ // Index capability_domain_model rows by capability_id for join.
20
+ const modelsByCap = new Map();
21
+ for (const row of inputs.capabilityDomainModelRows) {
22
+ const capId = String(row["capability_id"] ?? "");
23
+ const modelId = String(row["domain_model_id"] ?? "");
24
+ if (capId === "" || modelId === "")
25
+ continue;
26
+ const arr = modelsByCap.get(capId) ?? [];
27
+ arr.push(modelId);
28
+ modelsByCap.set(capId, arr);
29
+ }
30
+ const seen = new Map();
31
+ for (const row of inputs.capabilityFeatureRows) {
32
+ const featureId = String(row["feature_id"] ?? "");
33
+ const capId = String(row["capability_id"] ?? "");
34
+ if (featureId === "" || capId === "")
35
+ continue;
36
+ for (const modelId of modelsByCap.get(capId) ?? []) {
37
+ const id = `${featureId}__${modelId}`;
38
+ if (seen.has(id))
39
+ continue;
40
+ seen.set(id, {
41
+ id,
42
+ feature_id: featureId,
43
+ domain_model_id: modelId,
44
+ derivation_path: "cap-feature-cap-domain-model",
45
+ });
46
+ }
47
+ }
48
+ return Array.from(seen.values()).sort((a, b) => String(a["id"]).localeCompare(String(b["id"])));
49
+ }
50
+ //# sourceMappingURL=feature-domain-model.js.map
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Walker for `feature-grounding.md` files per ADR 0143 (schema 2.5.0).
3
+ *
4
+ * Reads markdown tables of the canonical shape:
5
+ *
6
+ * | Feature | UI Components | API Routes | Backend Handlers | Data Entities | Evidence |
7
+ * |---|---|---|---|---|---|
8
+ * | `oauth2-login` | ... | BFF `GET /login` | `AuthHandler.handle...()` | Token | `auth-handler.ts` |
9
+ *
10
+ * Path conventions (per ADR 0143 §4.3):
11
+ * - platform-profile: `docs/domains/<dom>/design/<capability>/feature-grounding.md`
12
+ * - integration-product: `docs/design/integration/feature-grounding.md`
13
+ *
14
+ * Tolerant of column re-ordering and missing optional columns. The only
15
+ * required column is `Feature`. Lines inside ```fenced``` code blocks
16
+ * are skipped. Recovery is row-local — a malformed row emits a finding
17
+ * and the walker continues with the next row.
18
+ */
19
+ import { readdir, readFile } from "node:fs/promises";
20
+ import { join, relative, sep } from "node:path";
21
+ const DOMAINS_DIR_REL = join("docs", "domains");
22
+ const PLATFORM_LEAF = "feature-grounding.md";
23
+ /**
24
+ * Find all `docs/domains/<dom>/design/<capability>/feature-grounding.md`
25
+ * files under the SDD root. Sorted for deterministic output.
26
+ */
27
+ export async function findPlatformGroundingFiles(sddPath) {
28
+ const out = [];
29
+ const domainsDir = join(sddPath, DOMAINS_DIR_REL);
30
+ let domains;
31
+ try {
32
+ domains = await readdir(domainsDir, { withFileTypes: true });
33
+ }
34
+ catch {
35
+ return out;
36
+ }
37
+ for (const d of domains) {
38
+ if (!d.isDirectory())
39
+ continue;
40
+ const designDir = join(domainsDir, d.name, "design");
41
+ let caps;
42
+ try {
43
+ caps = await readdir(designDir, { withFileTypes: true });
44
+ }
45
+ catch {
46
+ continue;
47
+ }
48
+ for (const c of caps) {
49
+ if (!c.isDirectory())
50
+ continue;
51
+ const candidate = join(designDir, c.name, PLATFORM_LEAF);
52
+ try {
53
+ await readFile(candidate, "utf-8");
54
+ out.push(candidate);
55
+ }
56
+ catch (err) {
57
+ const code = err.code;
58
+ if (code === "ENOENT")
59
+ continue;
60
+ throw err;
61
+ }
62
+ }
63
+ }
64
+ return out.sort();
65
+ }
66
+ /**
67
+ * Find the single integration-product feature-grounding.md, if any.
68
+ * Path: `docs/design/integration/feature-grounding.md`.
69
+ */
70
+ export async function findIntegrationGroundingFile(sddPath) {
71
+ const candidate = join(sddPath, "docs", "design", "integration", PLATFORM_LEAF);
72
+ try {
73
+ await readFile(candidate, "utf-8");
74
+ return candidate;
75
+ }
76
+ catch (err) {
77
+ const code = err.code;
78
+ if (code === "ENOENT")
79
+ return null;
80
+ throw err;
81
+ }
82
+ }
83
+ /**
84
+ * Resolve the capability id from a platform-profile grounding file's
85
+ * absolute path. Returns the parent folder name. Callers verify the
86
+ * id resolves against `capabilities.csv` and emit findings on miss.
87
+ */
88
+ export function capabilityIdFromPath(absPath, sddPath) {
89
+ const rel = relative(sddPath, absPath);
90
+ const parts = rel.split(sep);
91
+ // Expected: ["docs", "domains", "<dom>", "design", "<capability>", "feature-grounding.md"]
92
+ if (parts.length < 6)
93
+ return "";
94
+ return parts[parts.length - 2] ?? "";
95
+ }
96
+ /**
97
+ * Parse the markdown content of a feature-grounding.md file and return
98
+ * the rows from its first valid feature table. A "valid table" has a
99
+ * header row whose first non-empty cell normalises to `feature`.
100
+ * Returns an empty array if no such table is found (caller logs a
101
+ * coverage finding).
102
+ */
103
+ export function parseGroundingTable(text) {
104
+ const lines = text.split(/\r?\n/);
105
+ let inFence = false;
106
+ let i = 0;
107
+ while (i < lines.length) {
108
+ const raw = lines[i] ?? "";
109
+ if (isFenceToggle(raw)) {
110
+ inFence = !inFence;
111
+ i++;
112
+ continue;
113
+ }
114
+ if (inFence) {
115
+ i++;
116
+ continue;
117
+ }
118
+ if (!isTableRowLine(raw)) {
119
+ i++;
120
+ continue;
121
+ }
122
+ // Candidate header row. Need a separator row right after.
123
+ const next = lines[i + 1] ?? "";
124
+ if (!isSeparatorRowLine(next)) {
125
+ i++;
126
+ continue;
127
+ }
128
+ const headers = splitTableRow(raw).map(normaliseHeader);
129
+ if (headers.length === 0 || headers[0] !== "feature") {
130
+ i++;
131
+ continue;
132
+ }
133
+ // Found the table. Walk body rows until non-table line.
134
+ return parseBodyRows(lines, i + 2, headers);
135
+ }
136
+ return [];
137
+ }
138
+ function parseBodyRows(lines, startIdx, headers) {
139
+ const rows = [];
140
+ let inFence = false;
141
+ for (let i = startIdx; i < lines.length; i++) {
142
+ const raw = lines[i] ?? "";
143
+ if (isFenceToggle(raw)) {
144
+ inFence = !inFence;
145
+ continue;
146
+ }
147
+ if (inFence)
148
+ continue;
149
+ if (!isTableRowLine(raw)) {
150
+ // Allow a single blank line inside the table; anything else ends it.
151
+ if (raw.trim() === "")
152
+ continue;
153
+ break;
154
+ }
155
+ const cells = splitTableRow(raw);
156
+ const featureCell = cells[0] ?? "";
157
+ const slug = extractSlug(featureCell);
158
+ if (slug === "") {
159
+ // Skip rows whose first cell isn't a recognisable slug — caller logs.
160
+ continue;
161
+ }
162
+ const cellMap = new Map();
163
+ for (let c = 1; c < headers.length; c++) {
164
+ const header = headers[c] ?? "";
165
+ if (header === "")
166
+ continue;
167
+ cellMap.set(header, (cells[c] ?? "").trim());
168
+ }
169
+ rows.push({ slug, cells: cellMap, lineNumber: i + 1 });
170
+ }
171
+ return rows;
172
+ }
173
+ /** A line is a table row if it has at least one unescaped `|`. */
174
+ function isTableRowLine(raw) {
175
+ const trimmed = raw.trim();
176
+ if (!trimmed.startsWith("|") && !trimmed.includes("|"))
177
+ return false;
178
+ // Heuristic: a row must contain at least two | characters.
179
+ return (trimmed.match(/\|/g)?.length ?? 0) >= 2;
180
+ }
181
+ /** Separator row: `|---|---|...|`, possibly with `:` alignment markers. */
182
+ function isSeparatorRowLine(raw) {
183
+ const trimmed = raw.trim();
184
+ if (!trimmed.startsWith("|"))
185
+ return false;
186
+ return /^\|(\s*:?-+:?\s*\|)+\s*$/.test(trimmed);
187
+ }
188
+ function isFenceToggle(raw) {
189
+ const trimmed = raw.trimStart();
190
+ return trimmed.startsWith("```") || trimmed.startsWith("~~~");
191
+ }
192
+ /**
193
+ * Split a `| a | b | c |` row into cells. Strips leading/trailing
194
+ * empty cells caused by the bookend pipes. Does NOT split inside
195
+ * backtick spans — `` `foo|bar` `` stays as one cell.
196
+ */
197
+ function splitTableRow(raw) {
198
+ const cells = [];
199
+ let current = "";
200
+ let inTick = false;
201
+ // Skip the opening pipe if present.
202
+ let trimmed = raw.trim();
203
+ if (trimmed.startsWith("|"))
204
+ trimmed = trimmed.slice(1);
205
+ if (trimmed.endsWith("|"))
206
+ trimmed = trimmed.slice(0, -1);
207
+ for (const ch of trimmed) {
208
+ if (ch === "`") {
209
+ inTick = !inTick;
210
+ current += ch;
211
+ continue;
212
+ }
213
+ if (ch === "|" && !inTick) {
214
+ cells.push(current);
215
+ current = "";
216
+ continue;
217
+ }
218
+ current += ch;
219
+ }
220
+ cells.push(current);
221
+ return cells;
222
+ }
223
+ /**
224
+ * Normalise a header cell to a key form: lowercase, trimmed, spaces
225
+ * and `/` collapsed to `_`. Backticks stripped. e.g. "API Routes" →
226
+ * "api_routes", "UI / UX" → "ui_ux".
227
+ */
228
+ function normaliseHeader(cell) {
229
+ return cell
230
+ .replace(/`/g, "")
231
+ .trim()
232
+ .toLowerCase()
233
+ .replace(/[\s/]+/g, "_");
234
+ }
235
+ /**
236
+ * Extract the slug from the first cell. Recognises:
237
+ * - `` `slug` `` → "slug"
238
+ * - `` `slug` (description) `` → "slug"
239
+ * - plain `slug-name` → "slug-name"
240
+ * Returns "" if no valid slug pattern matches.
241
+ */
242
+ function extractSlug(cell) {
243
+ const trimmed = cell.trim();
244
+ // Backticked form first — the canonical shape.
245
+ const tickMatch = trimmed.match(/^`([a-z0-9][a-z0-9-]*)`/i);
246
+ if (tickMatch)
247
+ return tickMatch[1] ?? "";
248
+ // Plain kebab-case fallback.
249
+ const plainMatch = trimmed.match(/^([a-z][a-z0-9-]*)$/i);
250
+ if (plainMatch)
251
+ return plainMatch[1] ?? "";
252
+ return "";
253
+ }
254
+ //# sourceMappingURL=feature-grounding-walker.js.map