@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.
- package/LICENSE +201 -0
- package/NOTICE +17 -0
- package/README.md +6 -2
- package/dist/author-bin.js +8 -0
- package/dist/author-program.js +99 -0
- package/dist/lib/adr-origin.js +59 -0
- package/dist/lib/agent-harness.js +37 -0
- package/dist/lib/api-surface/__fixtures__/serverless-repo/serverless/serverless.yml +18 -0
- package/dist/lib/api-surface/analyzers/library.js +95 -0
- package/dist/lib/api-surface/analyzers/serverless.js +336 -0
- package/dist/lib/api-surface/analyzers/spring.js +237 -0
- package/dist/lib/api-surface/analyzers/terraform.js +109 -0
- package/dist/lib/api-surface/detect.js +25 -0
- package/dist/lib/api-surface/emit.js +109 -0
- package/dist/lib/api-surface/file-set.js +43 -0
- package/dist/lib/api-surface/index.js +134 -0
- package/dist/lib/api-surface/types.js +17 -0
- package/dist/lib/briefing/index.js +13 -0
- package/dist/lib/briefing/render.js +132 -0
- package/dist/lib/briefing/variables.js +92 -0
- package/dist/lib/catalogue/application-archetype/vocabulary.js +177 -0
- package/dist/lib/catalogue/archetype/maps/android.yaml +66 -0
- package/dist/lib/catalogue/archetype/maps/swiftui.yaml +58 -0
- package/dist/lib/catalogue/archetype/maps/uikit.yaml +48 -0
- package/dist/lib/catalogue/archetype/maps/web-generic.yaml +86 -0
- package/dist/lib/catalogue/archetype/openui-archetype-vocabulary.yaml +46 -0
- package/dist/lib/catalogue/archetype/resolver.js +103 -0
- package/dist/lib/catalogue/archetype/wireframe.js +418 -0
- package/dist/lib/catalogue/baseline-check.js +151 -0
- package/dist/lib/catalogue/build.js +1502 -0
- package/dist/lib/catalogue/builders/adr-supersedes.js +103 -0
- package/dist/lib/catalogue/builders/adrs.js +222 -0
- package/dist/lib/catalogue/builders/application-conformance-coverage.js +47 -0
- package/dist/lib/catalogue/builders/application-data-plane-bindings.js +115 -0
- package/dist/lib/catalogue/builders/c4-components.js +229 -0
- package/dist/lib/catalogue/builders/c4-containers.js +240 -0
- package/dist/lib/catalogue/builders/c4-entity-rollups.js +87 -0
- package/dist/lib/catalogue/builders/c4-external-systems.js +194 -0
- package/dist/lib/catalogue/builders/c4-l1-candidates.js +107 -0
- package/dist/lib/catalogue/builders/c4-systems.js +98 -0
- package/dist/lib/catalogue/builders/capabilities.js +488 -0
- package/dist/lib/catalogue/builders/capability-domain-model.js +74 -0
- package/dist/lib/catalogue/builders/capability-edges.js +256 -0
- package/dist/lib/catalogue/builders/capability-spec-walker.js +99 -0
- package/dist/lib/catalogue/builders/capability-spec.js +348 -0
- package/dist/lib/catalogue/builders/container-dependency.js +183 -0
- package/dist/lib/catalogue/builders/contracts.js +402 -0
- package/dist/lib/catalogue/builders/data-items.js +344 -0
- package/dist/lib/catalogue/builders/data-store-mapping.js +113 -0
- package/dist/lib/catalogue/builders/domain-models.js +175 -0
- package/dist/lib/catalogue/builders/domains.js +119 -0
- package/dist/lib/catalogue/builders/edge-label-merge.js +19 -0
- package/dist/lib/catalogue/builders/entities-manifest.js +160 -0
- package/dist/lib/catalogue/builders/entities-mermaid.js +189 -0
- package/dist/lib/catalogue/builders/entity-persistence.js +102 -0
- package/dist/lib/catalogue/builders/entity-slug.js +47 -0
- package/dist/lib/catalogue/builders/event-catalog-walker.js +186 -0
- package/dist/lib/catalogue/builders/event-edges.js +327 -0
- package/dist/lib/catalogue/builders/events.js +168 -0
- package/dist/lib/catalogue/builders/evidence-class.js +43 -0
- package/dist/lib/catalogue/builders/evidence-target-repo.js +67 -0
- package/dist/lib/catalogue/builders/external-system-exemption.js +120 -0
- package/dist/lib/catalogue/builders/feature-contract.js +77 -0
- package/dist/lib/catalogue/builders/feature-domain-model.js +50 -0
- package/dist/lib/catalogue/builders/feature-grounding-walker.js +254 -0
- package/dist/lib/catalogue/builders/feature-grounding.js +391 -0
- package/dist/lib/catalogue/builders/feature-groups.js +63 -0
- package/dist/lib/catalogue/builders/feature-subscription-channels.js +228 -0
- package/dist/lib/catalogue/builders/feature-tree-walker.js +295 -0
- package/dist/lib/catalogue/builders/features.js +300 -0
- package/dist/lib/catalogue/builders/glossary-xref.js +378 -0
- package/dist/lib/catalogue/builders/glossary.js +204 -0
- package/dist/lib/catalogue/builders/implementations.js +214 -0
- package/dist/lib/catalogue/builders/index.js +64 -0
- package/dist/lib/catalogue/builders/integration-components.js +344 -0
- package/dist/lib/catalogue/builders/integrations.js +181 -0
- package/dist/lib/catalogue/builders/persona-c4system.js +149 -0
- package/dist/lib/catalogue/builders/persona-container.js +133 -0
- package/dist/lib/catalogue/builders/persona-journey-edges.js +133 -0
- package/dist/lib/catalogue/builders/personas.js +152 -0
- package/dist/lib/catalogue/builders/product-edges.js +106 -0
- package/dist/lib/catalogue/builders/product-platform-dependencies.js +405 -0
- package/dist/lib/catalogue/builders/products.js +162 -0
- package/dist/lib/catalogue/builders/quality-scores.js +635 -0
- package/dist/lib/catalogue/builders/repo-capability-exemption.js +61 -0
- package/dist/lib/catalogue/builders/repo-dependency.js +127 -0
- package/dist/lib/catalogue/builders/repo-edges.js +340 -0
- package/dist/lib/catalogue/builders/repositories.js +458 -0
- package/dist/lib/catalogue/builders/scaffolds.js +197 -0
- package/dist/lib/catalogue/builders/screen-tree.js +222 -0
- package/dist/lib/catalogue/builders/screens.js +396 -0
- package/dist/lib/catalogue/builders/service-capability-exemption.js +63 -0
- package/dist/lib/catalogue/builders/service-contract-consumes.js +186 -0
- package/dist/lib/catalogue/builders/service-contract.js +74 -0
- package/dist/lib/catalogue/builders/service-entity-tables.js +187 -0
- package/dist/lib/catalogue/builders/services.js +232 -0
- package/dist/lib/catalogue/builders/system-external-dependency.js +115 -0
- package/dist/lib/catalogue/builders/user-journeys.js +253 -0
- package/dist/lib/catalogue/canonical-schema.yaml +4117 -0
- package/dist/lib/catalogue/cli-version.js +10 -0
- package/dist/lib/catalogue/contract-id.js +49 -0
- package/dist/lib/catalogue/coverage-predicates.js +603 -0
- package/dist/lib/catalogue/coverage-render.js +552 -0
- package/dist/lib/catalogue/coverage-run.js +115 -0
- package/dist/lib/catalogue/csv-reader.js +97 -0
- package/dist/lib/catalogue/csv-writer.js +96 -0
- package/dist/lib/catalogue/data-plane-coherence.js +77 -0
- package/dist/lib/catalogue/ddl-generator.js +233 -0
- package/dist/lib/catalogue/dedupe-keyed-tables.js +0 -0
- package/dist/lib/catalogue/engine-validator.js +239 -0
- package/dist/lib/catalogue/ext-id.js +76 -0
- package/dist/lib/catalogue/feature-grounding-aliases-loader.js +65 -0
- package/dist/lib/catalogue/feature-repo-zero-resolution-summary.js +31 -0
- package/dist/lib/catalogue/finding-identity.js +183 -0
- package/dist/lib/catalogue/findings.js +46 -0
- package/dist/lib/catalogue/fk-validator.js +136 -0
- package/dist/lib/catalogue/freshness.js +70 -0
- package/dist/lib/catalogue/gate-checks.js +338 -0
- package/dist/lib/catalogue/graph-projection.js +346 -0
- package/dist/lib/catalogue/impact-run.js +228 -0
- package/dist/lib/catalogue/manifest-loader.js +435 -0
- package/dist/lib/catalogue/platform-scope.js +7 -0
- package/dist/lib/catalogue/population-checks.js +87 -0
- package/dist/lib/catalogue/relationship-semantics.js +66 -0
- package/dist/lib/catalogue/reverse-product-quality.js +99 -0
- package/dist/lib/catalogue/rubric-coverage-map.yaml +871 -0
- package/dist/lib/catalogue/runway-vocabulary-loader.js +56 -0
- package/dist/lib/catalogue/scaffold-delta.js +261 -0
- package/dist/lib/catalogue/schema-compat.js +52 -0
- package/dist/lib/catalogue/schema-loader.js +87 -0
- package/dist/lib/catalogue/scores-coverage.js +573 -0
- package/dist/lib/catalogue/screen-validation-loader.js +89 -0
- package/dist/lib/catalogue/service-architecture-parser.js +193 -0
- package/dist/lib/catalogue/service-contract-aliases-loader.js +109 -0
- package/dist/lib/catalogue/staleness-verdicts.js +345 -0
- package/dist/lib/catalogue/traceability-loader.js +350 -0
- package/dist/lib/catalogue/validate.js +200 -0
- package/dist/lib/catalogue/version.js +18 -0
- package/dist/lib/conformance/binding.js +70 -0
- package/dist/lib/conformance/finding.js +69 -0
- package/dist/lib/conformance/index.js +578 -0
- package/dist/lib/context-pull/index.js +191 -0
- package/dist/lib/context-pull/resolve.js +70 -0
- package/dist/lib/context-pull/retired-mcp-setup.js +7 -0
- package/dist/lib/contracts-availability/index.js +267 -0
- package/dist/lib/generate-briefing/index.js +77 -0
- package/dist/lib/list/discover.js +100 -0
- package/dist/lib/list/format.js +84 -0
- package/dist/lib/list/index.js +16 -0
- package/dist/lib/lock-file.js +437 -0
- package/dist/lib/pack/log.js +18 -0
- package/dist/lib/pack/schema.js +121 -0
- package/dist/lib/pack/seed.js +182 -0
- package/dist/lib/pack/validate.js +49 -0
- package/dist/lib/package-version.js +92 -0
- package/dist/lib/plan/index.js +69 -0
- package/dist/lib/plan/read-sdd.js +102 -0
- package/dist/lib/plan/render-template.js +84 -0
- package/dist/lib/repo-kind.js +129 -0
- package/dist/lib/scaffold/domain.js +234 -0
- package/dist/lib/scaffold/registry.js +136 -0
- package/dist/lib/scripts.js +7 -0
- package/dist/lib/sdd-index/repo-config.js +245 -0
- package/dist/lib/sdd-reader.js +91 -0
- package/dist/lib/snapshot/advance.js +454 -0
- package/dist/lib/snapshot/diff.js +190 -0
- package/dist/lib/snapshot/fold.js +208 -0
- package/dist/lib/snapshot/land.js +178 -0
- package/dist/lib/snapshot/manifest.js +206 -0
- package/dist/lib/snapshot/targets.js +169 -0
- package/dist/lib/snapshot/verify.js +91 -0
- package/dist/lib/sync/briefing-merge.js +106 -0
- package/dist/lib/sync/bundle-cache.js +109 -0
- package/dist/lib/sync/yaml-block-merge.js +120 -0
- package/dist/lib/validate/aggregate.js +51 -0
- package/dist/lib/validate/bundle-coherence.js +74 -0
- package/dist/lib/validate/dispatch.js +243 -0
- package/dist/lib/validate/index.js +110 -0
- package/dist/lib/validate/plan.js +54 -0
- package/dist/lib/validate/runner.js +85 -0
- package/dist/lib/validate/yaml-parse.js +112 -0
- package/dist/maintain-version.json +4 -0
- package/dist/verbs/api-surface.js +71 -0
- package/dist/verbs/catalogue-check.js +28 -0
- package/dist/verbs/catalogue-verify.js +114 -0
- package/dist/verbs/conformance.js +34 -0
- package/dist/verbs/context.js +39 -0
- package/dist/verbs/contracts.js +34 -0
- package/dist/verbs/generate-briefing.js +29 -0
- package/dist/verbs/list.js +45 -0
- package/dist/verbs/pack-validate.js +17 -0
- package/dist/verbs/path-option.js +25 -0
- package/dist/verbs/plan.js +42 -0
- package/dist/verbs/render.js +87 -0
- package/dist/verbs/scaffold.js +83 -0
- package/dist/verbs/snapshot.js +134 -0
- package/dist/verbs/validate.js +48 -0
- package/dist/verify-program.js +122 -0
- package/dist/verify-version.json +4 -0
- package/package.json +32 -4
- package/scripts/validate-app-dependencies.sh +664 -0
- package/scripts/validate-app-docs.sh +199 -0
- 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
|