@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,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine-path FK/PK/enum validation — M-ADR 0205 §4.4 (Phase A2).
|
|
3
|
+
*
|
|
4
|
+
* Runs the ddl-generator's queries on the embedded engine and returns
|
|
5
|
+
* results in the SAME shapes as fk-validator.ts / enum-validator.ts, so
|
|
6
|
+
* the parity harness can compare count AND list, field for field. The
|
|
7
|
+
* input is the same rowsByCatalogue map the in-memory validators take:
|
|
8
|
+
* A2 compares the two VALIDATION implementations on identical parsed
|
|
9
|
+
* input — CSV-read equivalence is deliberately out of this comparison's
|
|
10
|
+
* scope and belongs to the cutover evaluation.
|
|
11
|
+
*
|
|
12
|
+
* The engine package is a devDependency until cutover (M-ADR 0205 §4.1's
|
|
13
|
+
* no-fallback rule governs how it eventually ships). It is imported
|
|
14
|
+
* dynamically inside the entry function so merely loading this module
|
|
15
|
+
* never requires it: no production path imports the engine today, and an
|
|
16
|
+
* adopter install without the package only fails if something actually
|
|
17
|
+
* calls it — loudly.
|
|
18
|
+
*
|
|
19
|
+
* PK-duplicate detection has NO in-memory counterpart at validate time
|
|
20
|
+
* (builders dedupe at emission instead), so pkDuplicates is engine-only
|
|
21
|
+
* information: reported, never part of the parity claim — you cannot
|
|
22
|
+
* claim agreement with an instrument that does not exist.
|
|
23
|
+
*/
|
|
24
|
+
import { generateConstraintQueries, generateExternalFindingQueries, generateTableDdl, sqlType, } from "./ddl-generator.js";
|
|
25
|
+
const INSERT_CHUNK = 500;
|
|
26
|
+
function sqlStringLiteral(value) {
|
|
27
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* One loaded engine: schema DDL applied, every provided catalogue's rows
|
|
31
|
+
* inserted. Shared by the FK/PK/enum path and the coverage predicates so
|
|
32
|
+
* there is exactly one load semantics to reason about.
|
|
33
|
+
*/
|
|
34
|
+
export async function createEngineSession(schema, rowsByCatalogue) {
|
|
35
|
+
const { DuckDBInstance } = await import("@duckdb/node-api");
|
|
36
|
+
const instance = await DuckDBInstance.create(":memory:");
|
|
37
|
+
const conn = await instance.connect();
|
|
38
|
+
for (const ddl of generateTableDdl(schema)) {
|
|
39
|
+
await conn.run(ddl);
|
|
40
|
+
}
|
|
41
|
+
await loadRows(conn, schema, rowsByCatalogue);
|
|
42
|
+
return { conn, dispose: () => conn.disconnectSync() };
|
|
43
|
+
}
|
|
44
|
+
// @rule engine-fk-validation
|
|
45
|
+
export async function validateWithEngine(schema, rowsByCatalogue, options = {}) {
|
|
46
|
+
const session = await createEngineSession(schema, rowsByCatalogue);
|
|
47
|
+
const conn = session.conn;
|
|
48
|
+
try {
|
|
49
|
+
const typeNonconformances = await findTypeNonconformances(conn, schema, rowsByCatalogue);
|
|
50
|
+
const failures = [];
|
|
51
|
+
const enumViolations = [];
|
|
52
|
+
const pkDuplicates = [];
|
|
53
|
+
for (const q of generateConstraintQueries(schema, options)) {
|
|
54
|
+
const reader = await conn.runAndReadAll(q.sql);
|
|
55
|
+
for (const row of reader.getRows()) {
|
|
56
|
+
if (q.kind === "fk") {
|
|
57
|
+
failures.push({
|
|
58
|
+
catalogue: q.catalogue,
|
|
59
|
+
column: q.column,
|
|
60
|
+
referencingRow: String(row[0] ?? ""),
|
|
61
|
+
value: String(row[1] ?? ""),
|
|
62
|
+
target: q.target,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
else if (q.kind === "enum") {
|
|
66
|
+
const enumDef = schema.enums[q.target];
|
|
67
|
+
enumViolations.push({
|
|
68
|
+
catalogue: q.catalogue,
|
|
69
|
+
column: q.column,
|
|
70
|
+
rowPk: row[0] === null || row[0] === undefined ? "<no-pk>" : String(row[0]),
|
|
71
|
+
value: String(row[1] ?? ""),
|
|
72
|
+
allowed: enumDef.values.map((v) => v.value).join(" | "),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
pkDuplicates.push({
|
|
77
|
+
catalogue: q.catalogue,
|
|
78
|
+
rowPk: String(row[0] ?? ""),
|
|
79
|
+
count: Number(row[1]),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const externalFindingDetails = [];
|
|
85
|
+
for (const q of generateExternalFindingQueries(schema, options)) {
|
|
86
|
+
const reader = await conn.runAndReadAll(q.sql);
|
|
87
|
+
for (const row of reader.getRows()) {
|
|
88
|
+
externalFindingDetails.push({
|
|
89
|
+
catalogue: q.catalogue,
|
|
90
|
+
column: q.column,
|
|
91
|
+
target: q.target,
|
|
92
|
+
rowPk: row[0] === null || row[0] === undefined ? "?" : String(row[0]),
|
|
93
|
+
value: String(row[1] ?? ""),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
fk: {
|
|
99
|
+
internalPass: failures.length === 0,
|
|
100
|
+
internalFailures: failures,
|
|
101
|
+
externalFindings: externalFindingDetails.length,
|
|
102
|
+
},
|
|
103
|
+
enums: { pass: enumViolations.length === 0, violations: enumViolations },
|
|
104
|
+
externalFindingDetails,
|
|
105
|
+
pkDuplicates,
|
|
106
|
+
typeNonconformances,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
session.dispose();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The A4 cutover surface: runs the engine validation and writes findings
|
|
115
|
+
* into the caller's FindingsLog with BYTE-IDENTICAL text to what
|
|
116
|
+
* fk-validator.ts / enum-validator.ts wrote — adopters' committed
|
|
117
|
+
* findings baselines hold those literal lines, so the text is a
|
|
118
|
+
* compatibility contract, not a style choice. Line ORDER within a
|
|
119
|
+
* category follows the engine's total ORDER BY (M-ADR 0205 §4.2) and may
|
|
120
|
+
* differ from the old file-order rendering; baselines compare by
|
|
121
|
+
* membership, so ordering is free — declared in the change-set anyway.
|
|
122
|
+
*/
|
|
123
|
+
export async function validateWithEngineToLog(schema, rowsByCatalogue, findings, options = {}, extras = {}) {
|
|
124
|
+
const logEnumFindings = extras.logEnumFindings !== false;
|
|
125
|
+
const res = await validateWithEngine(schema, rowsByCatalogue, options);
|
|
126
|
+
for (const d of res.externalFindingDetails) {
|
|
127
|
+
findings.add("external", `${d.catalogue}.${d.column} → ${d.target}=${d.value} ` +
|
|
128
|
+
`(referencing row pk=${d.rowPk}; resolves at aggregation)`);
|
|
129
|
+
}
|
|
130
|
+
if (logEnumFindings) {
|
|
131
|
+
const enumNameByCatalogueColumn = new Map();
|
|
132
|
+
for (const cat of schema.catalogues) {
|
|
133
|
+
for (const [column, enumRef] of Object.entries(cat.columnEnums)) {
|
|
134
|
+
enumNameByCatalogueColumn.set(`${cat.name}.${column}`, enumRef.replace(/^enums\./, ""));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
for (const v of res.enums.violations) {
|
|
138
|
+
const enumName = enumNameByCatalogueColumn.get(`${v.catalogue}.${v.column}`);
|
|
139
|
+
findings.add("divergence", `${v.catalogue}.${v.column}: row ${v.rowPk} value "${v.value}" is outside the bound enum ${enumName} (${v.allowed})`, "error");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { fk: res.fk, enums: res.enums };
|
|
143
|
+
}
|
|
144
|
+
async function loadRows(conn, schema, rowsByCatalogue) {
|
|
145
|
+
for (const cat of schema.catalogues) {
|
|
146
|
+
const rows = rowsByCatalogue.get(cat.name) ?? [];
|
|
147
|
+
if (rows.length === 0)
|
|
148
|
+
continue;
|
|
149
|
+
for (let i = 0; i < rows.length; i += INSERT_CHUNK) {
|
|
150
|
+
const chunk = rows.slice(i, i + INSERT_CHUNK);
|
|
151
|
+
const tuples = chunk
|
|
152
|
+
.map((row) => `(${cat.columns
|
|
153
|
+
.map((c) => literal(row[c.name], cat.name, c.name, c.type))
|
|
154
|
+
.join(", ")})`)
|
|
155
|
+
.join(",\n");
|
|
156
|
+
await conn.run(`INSERT INTO "${cat.name}" VALUES\n${tuples};`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Renders one cell. String columns keep the empty string AS an empty
|
|
162
|
+
* string — the in-memory validators' emptiness predicates see '' from the
|
|
163
|
+
* CSV reader, and the generated queries test both NULL and ''. Non-string
|
|
164
|
+
* columns TRY_CAST with '' becoming NULL: one uncastable cell must not
|
|
165
|
+
* abort a whole run (measured on a real estate: a prose value in a
|
|
166
|
+
* date-typed column), and the miss is not smoothed either —
|
|
167
|
+
* findTypeNonconformances re-derives every such cell with the engine's
|
|
168
|
+
* own cast semantics and reports it.
|
|
169
|
+
*/
|
|
170
|
+
function literal(value, catalogue, column, type) {
|
|
171
|
+
const s = value === null || value === undefined ? "" : String(value);
|
|
172
|
+
if (type === "string")
|
|
173
|
+
return sqlStringLiteral(s);
|
|
174
|
+
if (s === "")
|
|
175
|
+
return "NULL";
|
|
176
|
+
return `TRY_CAST(${sqlStringLiteral(s)} AS ${sqlType(catalogue, column, type)})`;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Exact engine-semantics detection of the cells TRY_CAST nulled: the
|
|
180
|
+
* distinct non-empty values of every typed column are re-checked with the
|
|
181
|
+
* SAME cast the load used — a JS re-implementation of the engine's parsers
|
|
182
|
+
* would be a second opinion pretending to be the first.
|
|
183
|
+
*/
|
|
184
|
+
async function findTypeNonconformances(conn, schema, rowsByCatalogue) {
|
|
185
|
+
const out = [];
|
|
186
|
+
for (const cat of schema.catalogues) {
|
|
187
|
+
const rows = rowsByCatalogue.get(cat.name) ?? [];
|
|
188
|
+
if (rows.length === 0)
|
|
189
|
+
continue;
|
|
190
|
+
for (const col of cat.columns) {
|
|
191
|
+
if (col.type === "string")
|
|
192
|
+
continue;
|
|
193
|
+
const counts = new Map();
|
|
194
|
+
for (const row of rows) {
|
|
195
|
+
const v = row[col.name];
|
|
196
|
+
const s = v === null || v === undefined ? "" : String(v);
|
|
197
|
+
if (s === "")
|
|
198
|
+
continue;
|
|
199
|
+
counts.set(s, (counts.get(s) ?? 0) + 1);
|
|
200
|
+
}
|
|
201
|
+
if (counts.size === 0)
|
|
202
|
+
continue;
|
|
203
|
+
const engineType = sqlType(cat.name, col.name, col.type);
|
|
204
|
+
const values = [...counts.keys()].sort();
|
|
205
|
+
const tuples = values.map((v) => `(${sqlStringLiteral(v)})`).join(", ");
|
|
206
|
+
const reader = await conn.runAndReadAll(`SELECT v FROM (VALUES ${tuples}) t(v) ` +
|
|
207
|
+
`WHERE TRY_CAST(v AS ${engineType}) IS NULL ORDER BY v;`);
|
|
208
|
+
for (const row of reader.getRows()) {
|
|
209
|
+
const value = String(row[0]);
|
|
210
|
+
out.push({
|
|
211
|
+
catalogue: cat.name,
|
|
212
|
+
column: col.name,
|
|
213
|
+
declaredType: col.type,
|
|
214
|
+
value,
|
|
215
|
+
rows: counts.get(value) ?? 0,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* The population a PASS was measured over, for the verdict line. An FK or enum
|
|
224
|
+
* PASS over zero rows is true of nothing (empty-set sweep, R2): the count makes
|
|
225
|
+
* the zero visible. The `PASS` token itself is kept for anything that greps it.
|
|
226
|
+
*/
|
|
227
|
+
export function validatedPopulation(rowsByCatalogue) {
|
|
228
|
+
let rows = 0;
|
|
229
|
+
let nonEmpty = 0;
|
|
230
|
+
for (const r of rowsByCatalogue.values()) {
|
|
231
|
+
rows += r.length;
|
|
232
|
+
if (r.length > 0)
|
|
233
|
+
nonEmpty += 1;
|
|
234
|
+
}
|
|
235
|
+
return rows === 0
|
|
236
|
+
? "0 rows — nothing was validated"
|
|
237
|
+
: `${rows} row(s) across ${nonEmpty} non-empty catalogue(s)`;
|
|
238
|
+
}
|
|
239
|
+
//# sourceMappingURL=engine-validator.js.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// External-system row identity (schema 3.0.0) and the legacy-id migration
|
|
2
|
+
// map — M-ADR 0203 §4.2, completing M-ADR 0189 §4.2.
|
|
3
|
+
//
|
|
4
|
+
// From 3.0.0 a `c4_external_systems` row id derives from the DECLARED
|
|
5
|
+
// diagram entry id (`System_Ext(<entry-id>, …)`), not from a slug of the
|
|
6
|
+
// display name. The declared id is the identity the aggregator resolves
|
|
7
|
+
// (M-ADR 0189 §4.2); keying the row on anything else meant the row's own
|
|
8
|
+
// PK was label-keyed evidence — the exact identity class 0189 measured
|
|
9
|
+
// false-positiving at 14 of 15.
|
|
10
|
+
//
|
|
11
|
+
// The 2.x name-slug ids remain derivable from data every 2.30.0+ catalogue
|
|
12
|
+
// carries (`name`), so the old→new map is a build artefact, not an authored
|
|
13
|
+
// migration: `sdd-cli catalogue migration-map` emits it, and the exemption
|
|
14
|
+
// loader consults it during the cut window (an authored exemption keyed by
|
|
15
|
+
// a mapped old id is regenerable, NOT drift — the F30 rule at schema scale).
|
|
16
|
+
/**
|
|
17
|
+
* Normalise a declared diagram entry id to slug form. Camel humps split
|
|
18
|
+
* BEFORE lowercasing (the F11a rule, matching the aggregator's resolver):
|
|
19
|
+
* most SDD ids are hyphenated, and `bvfmGatewayFramework` must derive the
|
|
20
|
+
* same identity the adjudicator resolves for it (`bvfm-gateway-framework`).
|
|
21
|
+
*/
|
|
22
|
+
export function normaliseEntryId(entryId) {
|
|
23
|
+
return entryId
|
|
24
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
27
|
+
.replace(/^-+|-+$/g, "");
|
|
28
|
+
}
|
|
29
|
+
/** The 3.0.0 row id for a declared diagram entry id, or "" when the entry id normalises to nothing. */
|
|
30
|
+
export function extIdFromEntryId(entryId) {
|
|
31
|
+
const slug = normaliseEntryId(entryId);
|
|
32
|
+
return slug === "" ? "" : `ext-${slug}`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The 2.x row id for a display name — the pre-3.0.0 derivation, kept
|
|
36
|
+
* VERBATIM (including the parenthetical strip) so the migration map
|
|
37
|
+
* reproduces exactly the ids the old builder minted.
|
|
38
|
+
*/
|
|
39
|
+
export function legacyExtIdFromName(name) {
|
|
40
|
+
const slug = name
|
|
41
|
+
.toLowerCase()
|
|
42
|
+
.replace(/\s*\([^)]+\)/g, "")
|
|
43
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
44
|
+
.replace(/^-+|-+$/g, "");
|
|
45
|
+
return slug === "" ? "" : `ext-${slug}`;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* legacy (2.x, name-derived) id → the 3.0.0 ids it maps to, from built
|
|
49
|
+
* rows. Usually one; MORE than one means two distinct declared identities
|
|
50
|
+
* shared a display-name slug and were wrongly merged as one row under 2.x
|
|
51
|
+
* — the map surfaces the split rather than guessing (the consumer decides,
|
|
52
|
+
* loudly).
|
|
53
|
+
*/
|
|
54
|
+
export function buildLegacyExtIdMap(rows) {
|
|
55
|
+
const map = new Map();
|
|
56
|
+
for (const row of rows) {
|
|
57
|
+
const legacy = legacyExtIdFromName(String(row["name"] ?? ""));
|
|
58
|
+
if (legacy === "")
|
|
59
|
+
continue;
|
|
60
|
+
const current = String(row["id"] ?? "");
|
|
61
|
+
if (current === "" || legacy === current)
|
|
62
|
+
continue;
|
|
63
|
+
const ids = map.get(legacy);
|
|
64
|
+
if (ids) {
|
|
65
|
+
if (!ids.includes(current))
|
|
66
|
+
ids.push(current);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
map.set(legacy, [current]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (const ids of map.values())
|
|
73
|
+
ids.sort((a, b) => a.localeCompare(b));
|
|
74
|
+
return map;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=ext-id.js.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loader for the optional `.sdd/feature-grounding-aliases.yaml` config
|
|
3
|
+
* per ADR 0143 (schema 2.5.0).
|
|
4
|
+
*
|
|
5
|
+
* Adopters use this map to translate display names in
|
|
6
|
+
* `feature-grounding.md` tables (class names, file basenames, route
|
|
7
|
+
* fragments) into canonical service / repo ids that resolve against
|
|
8
|
+
* `services.csv` and `repositories.csv`. Follows the same shape
|
|
9
|
+
* convention as the existing capability_service alias map.
|
|
10
|
+
*
|
|
11
|
+
* File shape:
|
|
12
|
+
*
|
|
13
|
+
* service_aliases:
|
|
14
|
+
* AuthHandler: example-service-api
|
|
15
|
+
* web-ui: example-web-ui
|
|
16
|
+
* repo_aliases:
|
|
17
|
+
* camera-bff: global-camera-bff
|
|
18
|
+
*
|
|
19
|
+
* Either block is optional. Empty / missing file = no aliases (every
|
|
20
|
+
* reference must resolve directly against the canonical id).
|
|
21
|
+
*/
|
|
22
|
+
import { readFile } from "node:fs/promises";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { parse as parseYaml } from "yaml";
|
|
25
|
+
const ALIASES_FILENAME = join(".sdd", "feature-grounding-aliases.yaml");
|
|
26
|
+
/**
|
|
27
|
+
* Load the optional alias map. Returns `null` when the file is absent
|
|
28
|
+
* — callers treat that as "no aliases, all references must resolve
|
|
29
|
+
* directly against the canonical id." Throws on malformed YAML.
|
|
30
|
+
*/
|
|
31
|
+
export async function loadFeatureGroundingAliases(sddPath) {
|
|
32
|
+
const full = join(sddPath, ALIASES_FILENAME);
|
|
33
|
+
let text;
|
|
34
|
+
try {
|
|
35
|
+
text = await readFile(full, "utf-8");
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
const code = err.code;
|
|
39
|
+
if (code === "ENOENT")
|
|
40
|
+
return null;
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
const raw = parseYaml(text);
|
|
44
|
+
if (raw === null || typeof raw !== "object") {
|
|
45
|
+
throw new Error(`${ALIASES_FILENAME}: expected a mapping at the document root`);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
serviceAliases: toStringMap(raw.service_aliases),
|
|
49
|
+
repoAliases: toStringMap(raw.repo_aliases),
|
|
50
|
+
relPath: ALIASES_FILENAME,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function toStringMap(raw) {
|
|
54
|
+
const out = new Map();
|
|
55
|
+
if (raw === null || raw === undefined)
|
|
56
|
+
return out;
|
|
57
|
+
if (typeof raw !== "object" || Array.isArray(raw))
|
|
58
|
+
return out;
|
|
59
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
60
|
+
if (typeof v === "string" && v !== "")
|
|
61
|
+
out.set(k, v);
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=feature-grounding-aliases-loader.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* feature_repo zero-resolution SUMMARY line (M-ADR 0143 §4.4), split against
|
|
3
|
+
* the final feature_repo table.
|
|
4
|
+
*
|
|
5
|
+
* A builder (feature-grounding: Evidence cell; capability-spec:
|
|
6
|
+
* Implementation cell) fires one per-row finding for every feature row whose
|
|
7
|
+
* cell is non-empty but resolves to zero repos. That measures the CELL. The
|
|
8
|
+
* feature may still be anchored by another surface — feature-tree
|
|
9
|
+
* `## Feature Services`, feature-spec, or the feature_service ⋈
|
|
10
|
+
* service_repo derivation — and only the build, holding the final
|
|
11
|
+
* feature_repo, can say so. The summary therefore states both numbers:
|
|
12
|
+
* how many rows fired, and how many of those fired on features that no
|
|
13
|
+
* surface anchors.
|
|
14
|
+
*
|
|
15
|
+
* Returns null when nothing fired: an absent line is the designed shape
|
|
16
|
+
* for zero (the per-row lines are absent too), and this keeps the finding
|
|
17
|
+
* surface identical to pre-1.20.2 in that case.
|
|
18
|
+
*/
|
|
19
|
+
export function zeroResolutionSummary(cellLabel, evidenceRows, firedFeatureIds, anchoredFeatures) {
|
|
20
|
+
if (firedFeatureIds.length === 0)
|
|
21
|
+
return null;
|
|
22
|
+
const distinctFired = new Set(firedFeatureIds);
|
|
23
|
+
const unanchoredFeatures = new Set([...distinctFired].filter((fid) => !anchoredFeatures.has(fid)));
|
|
24
|
+
const unanchoredRows = firedFeatureIds.filter((fid) => unanchoredFeatures.has(fid)).length;
|
|
25
|
+
const anchoredElsewhere = distinctFired.size - unanchoredFeatures.size;
|
|
26
|
+
return (`feature_repo: ${firedFeatureIds.length} of ${evidenceRows} feature rows with a non-empty ${cellLabel} cell resolved to zero repos, ` +
|
|
27
|
+
`of which ${unanchoredRows} on ${unanchoredFeatures.size} feature(s) with no anchor from ANY surface in the final feature_repo ` +
|
|
28
|
+
`(${anchoredElsewhere} of the ${distinctFired.size} named features are anchored by another surface — those rows are cell quality, not coverage) ` +
|
|
29
|
+
`(member tier; evidence→repo resolution coverage only — a resolved row is not thereby correct; M-ADR 0143 §4.4)`);
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=feature-repo-zero-resolution-summary.js.map
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finding IDENTITY — the stable key a findings baseline is compared on, per
|
|
3
|
+
* M-ADR 0132 §4.3 as amended, delivering the ruling recorded at ledger §3p.
|
|
4
|
+
*
|
|
5
|
+
* THE PROBLEM THIS EXISTS FOR, measured rather than asserted. A findings
|
|
6
|
+
* baseline stores whole `[finding]` lines, so improving a message invalidates
|
|
7
|
+
* every baselined line carrying it. Wave №6 added a remedy sentence to one
|
|
8
|
+
* orphan message: 57 baselined lines went inert and 57 replacements arrived
|
|
9
|
+
* unbaselined, with ZERO genuinely new findings underneath. At the baseline, a
|
|
10
|
+
* wave that improves wording is indistinguishable from a wave that introduces
|
|
11
|
+
* 57 new problems. It recurred at wave №11 (9 lines, one estate) because the
|
|
12
|
+
* class was understood and the mechanism had not changed.
|
|
13
|
+
*
|
|
14
|
+
* WHY IDENTITY IS EMITTED AND NOT RECOVERED. The obvious cheaper fix is to let
|
|
15
|
+
* a comparator extract (category, subject) from stored lines and leave every
|
|
16
|
+
* estate file untouched. Measured across every baseline in both estates — 86
|
|
17
|
+
* files, 74 non-empty, 1,677 body lines — that works for 1,434 lines (85.5%)
|
|
18
|
+
* and fails for 243 (14.5%), which embed their subject in prose:
|
|
19
|
+
*
|
|
20
|
+
* [finding] orphan: repo integration-calamp resolves to no capability ...
|
|
21
|
+
*
|
|
22
|
+
* One renderer template, all orphan class, spread across 62 repos. Recovering
|
|
23
|
+
* identity there means teaching the comparator the renderer's message
|
|
24
|
+
* templates — which re-couples identity to message text, the exact coupling
|
|
25
|
+
* this module removes, and fails SILENTLY the next time a template changes.
|
|
26
|
+
* Identity that was never emitted cannot be reconstructed from prose after the
|
|
27
|
+
* fact; it has to be emitted where it is known.
|
|
28
|
+
*
|
|
29
|
+
* THE RENDERED FORM keeps the identity in the line a human reads, because a
|
|
30
|
+
* baseline is reviewed by people and a side-channel nobody sees is a side
|
|
31
|
+
* channel nobody maintains:
|
|
32
|
+
*
|
|
33
|
+
* [finding] orphan (repo:integration-calamp): repo integration-calamp ...
|
|
34
|
+
* [finding] divergence (docs/domains/asset/product/x.traceability.yaml): ...
|
|
35
|
+
*
|
|
36
|
+
* A finding with no declared subject renders EXACTLY as it does today. That is
|
|
37
|
+
* deliberate and load-bearing: migration is per-emission-site rather than
|
|
38
|
+
* flag-day, an unmigrated site is unchanged rather than broken, and the size of
|
|
39
|
+
* an estate's re-take is proportional to how many sites have actually moved.
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* A subject is `kind:id` where the finding knows the kind (`repo:x`,
|
|
43
|
+
* `capability:y`), or a repo-relative path where the subject IS a file. Both
|
|
44
|
+
* are stable under rewording; neither is a sentence.
|
|
45
|
+
*
|
|
46
|
+
* Parentheses are the delimiter because no subject may contain one — asserted
|
|
47
|
+
* below rather than assumed, since a subject that could contain the delimiter
|
|
48
|
+
* would make the parse ambiguous exactly when it mattered.
|
|
49
|
+
*/
|
|
50
|
+
export function assertRenderableSubject(subject) {
|
|
51
|
+
if (subject.includes("(") || subject.includes(")")) {
|
|
52
|
+
throw new Error(`finding subject may not contain parentheses (they delimit the identity): ${subject}`);
|
|
53
|
+
}
|
|
54
|
+
if (subject.trim() !== subject || subject === "") {
|
|
55
|
+
throw new Error(`finding subject must be non-empty and untrimmed-clean: '${subject}'`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function renderFindingLine(f) {
|
|
59
|
+
const err = f.severity === "error" ? " [error]" : "";
|
|
60
|
+
if (f.subject === undefined || f.subject === null) {
|
|
61
|
+
return `[finding] ${f.category}${err}: ${f.summary}`;
|
|
62
|
+
}
|
|
63
|
+
assertRenderableSubject(f.subject);
|
|
64
|
+
return `[finding] ${f.category}${err} (${f.subject}): ${f.summary}`;
|
|
65
|
+
}
|
|
66
|
+
const EXPLICIT = /^\[finding\] (\w+)(?: \[error\])? \(([^)]+)\): /;
|
|
67
|
+
const LEGACY = /^\[finding\] (\w+)(?: \[error\])?: /;
|
|
68
|
+
/**
|
|
69
|
+
* Recovers the identity of a stored baseline line.
|
|
70
|
+
*
|
|
71
|
+
* Three outcomes, and the third is the one that matters: an explicit identity
|
|
72
|
+
* is read exactly; a LEGACY line is keyed on its whole summary, which
|
|
73
|
+
* reproduces today's whole-line behaviour for lines that have not moved yet;
|
|
74
|
+
* and a line that is not a finding at all returns null.
|
|
75
|
+
*
|
|
76
|
+
* Keying a legacy line on its summary is not a silent approximation of
|
|
77
|
+
* identity — it IS the old contract, kept deliberately so an unmigrated line
|
|
78
|
+
* behaves exactly as it does now rather than matching something it should not.
|
|
79
|
+
*/
|
|
80
|
+
export function parseFindingLine(line) {
|
|
81
|
+
const m = EXPLICIT.exec(line);
|
|
82
|
+
if (m) {
|
|
83
|
+
return { identity: { category: m[1], subject: m[2] }, explicit: true };
|
|
84
|
+
}
|
|
85
|
+
const l = LEGACY.exec(line);
|
|
86
|
+
if (!l)
|
|
87
|
+
return null;
|
|
88
|
+
return {
|
|
89
|
+
identity: { category: l[1], subject: null },
|
|
90
|
+
explicit: false,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The comparison key. For an explicit identity that is (category, subject); for
|
|
95
|
+
* a legacy line it falls back to the whole line, preserving current behaviour.
|
|
96
|
+
*/
|
|
97
|
+
export function findingKey(line) {
|
|
98
|
+
const parsed = parseFindingLine(line);
|
|
99
|
+
if (!parsed)
|
|
100
|
+
return `raw\u0000${line}`;
|
|
101
|
+
if (parsed.identity.subject === null)
|
|
102
|
+
return `legacy\u0000${line}`;
|
|
103
|
+
return `id\u0000${parsed.identity.category}\u0000${parsed.identity.subject}`;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The MIGRATION BRIDGE: a live identity-carrying line with its identity token
|
|
107
|
+
* removed, which is byte-identical to how that same finding rendered before its
|
|
108
|
+
* emission site migrated.
|
|
109
|
+
*
|
|
110
|
+
* This is not inference. The token is exactly the ` (subject)` this module's own
|
|
111
|
+
* renderer inserts, and the summary either side of it is untouched — so removing
|
|
112
|
+
* it reconstructs the previous rendering deterministically, not plausibly.
|
|
113
|
+
*
|
|
114
|
+
* WHY IT EXISTS, measured on a real estate: a baseline dominated by findings
|
|
115
|
+
* from the FIRST migrated emission sites read 629 unbaselined / 380 closed / 0
|
|
116
|
+
* REWORDED — one closed and one new, which is the exact outcome §9 was written
|
|
117
|
+
* to prevent. The rewording bucket could not catch it because rewording is
|
|
118
|
+
* matched between two identity-carrying lines, and a pre-migration baseline has
|
|
119
|
+
* no identity on its side. The mechanism worked; the migration boundary sat
|
|
120
|
+
* underneath it, invisible to any estate whose baseline predates emission.
|
|
121
|
+
*/
|
|
122
|
+
export function strippedOfIdentity(line) {
|
|
123
|
+
const m = EXPLICIT.exec(line);
|
|
124
|
+
if (!m)
|
|
125
|
+
return null;
|
|
126
|
+
return line.replace(` (${m[2]})`, "");
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Compares live findings against a baseline by identity.
|
|
130
|
+
*
|
|
131
|
+
* Only gate-acting categories belong in a baseline (M-ADR 0132 §4.3 — `orphan`
|
|
132
|
+
* and `divergence`); filtering is the caller's, because a comparator that
|
|
133
|
+
* silently dropped categories would make an empty result ambiguous between
|
|
134
|
+
* "nothing gate-acting" and "nothing at all".
|
|
135
|
+
*/
|
|
136
|
+
export function compareToBaseline(live, baseline) {
|
|
137
|
+
const baseByKey = new Map();
|
|
138
|
+
let legacyKeyed = 0;
|
|
139
|
+
for (const b of baseline) {
|
|
140
|
+
const key = findingKey(b);
|
|
141
|
+
if (key.startsWith("legacy\u0000") || key.startsWith("raw\u0000"))
|
|
142
|
+
legacyKeyed++;
|
|
143
|
+
// First occurrence wins; a baseline holding the same identity twice is a
|
|
144
|
+
// duplicate, and collapsing it here matches what a re-take does anyway.
|
|
145
|
+
if (!baseByKey.has(key))
|
|
146
|
+
baseByKey.set(key, b);
|
|
147
|
+
}
|
|
148
|
+
const unbaselined = [];
|
|
149
|
+
const reworded = [];
|
|
150
|
+
const matched = new Set();
|
|
151
|
+
let unchanged = 0;
|
|
152
|
+
for (const l of live) {
|
|
153
|
+
const key = findingKey(l);
|
|
154
|
+
let b = baseByKey.get(key);
|
|
155
|
+
if (b === undefined) {
|
|
156
|
+
// MIGRATION BRIDGE: before calling a finding new, ask whether this is the
|
|
157
|
+
// same finding wearing its pre-migration rendering. Exact match on the
|
|
158
|
+
// stripped form only — never a fuzzy one.
|
|
159
|
+
const stripped = strippedOfIdentity(l);
|
|
160
|
+
if (stripped !== null) {
|
|
161
|
+
const legacyKey = findingKey(stripped);
|
|
162
|
+
const legacyMatch = baseByKey.get(legacyKey);
|
|
163
|
+
if (legacyMatch !== undefined) {
|
|
164
|
+
matched.add(legacyKey);
|
|
165
|
+
reworded.push({ baseline: legacyMatch, live: l });
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
unbaselined.push(l);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
matched.add(key);
|
|
173
|
+
if (b === l)
|
|
174
|
+
unchanged++;
|
|
175
|
+
else
|
|
176
|
+
reworded.push({ baseline: b, live: l });
|
|
177
|
+
}
|
|
178
|
+
const stale = [...baseByKey.entries()]
|
|
179
|
+
.filter(([k]) => !matched.has(k))
|
|
180
|
+
.map(([, v]) => v);
|
|
181
|
+
return { unbaselined, stale, reworded, unchanged, legacyKeyed };
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=finding-identity.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Findings collector for the canonical builder per ADR 0134 / builder-spec
|
|
3
|
+
* §"Emit findings as it parses".
|
|
4
|
+
*
|
|
5
|
+
* Findings are human-readable AND machine-parseable. Format per finding:
|
|
6
|
+
* [finding] {category}: {one-line summary}
|
|
7
|
+
*
|
|
8
|
+
* Categories are an open set; the canonical catalogue is in the builder
|
|
9
|
+
* spec. This module hardcodes the well-known ones for type safety; new
|
|
10
|
+
* categories may be passed as freeform strings if they emerge.
|
|
11
|
+
*/
|
|
12
|
+
import { renderFindingLine } from "./finding-identity.js";
|
|
13
|
+
export class FindingsLog {
|
|
14
|
+
findings = [];
|
|
15
|
+
add(category, summary, severity = "warn", subject) {
|
|
16
|
+
// Spread rather than an explicit `subject: undefined`, which
|
|
17
|
+
// exactOptionalPropertyTypes rejects — an absent subject and a subject
|
|
18
|
+
// whose value is undefined are different things here, and the type is
|
|
19
|
+
// right to insist on the distinction.
|
|
20
|
+
this.findings.push({
|
|
21
|
+
category,
|
|
22
|
+
summary,
|
|
23
|
+
severity,
|
|
24
|
+
...(subject === undefined ? {} : { subject }),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/** Returns a copy of recorded findings, in insertion order. */
|
|
28
|
+
all() {
|
|
29
|
+
return [...this.findings];
|
|
30
|
+
}
|
|
31
|
+
/** Count findings, optionally filtered to a single category. */
|
|
32
|
+
count(category) {
|
|
33
|
+
if (category === undefined)
|
|
34
|
+
return this.findings.length;
|
|
35
|
+
return this.findings.filter((f) => f.category === category).length;
|
|
36
|
+
}
|
|
37
|
+
/** Count error-severity findings (these fail the build by default). */
|
|
38
|
+
errorCount() {
|
|
39
|
+
return this.findings.filter((f) => f.severity === "error").length;
|
|
40
|
+
}
|
|
41
|
+
/** Render every finding as a stdout line. Error-severity findings are marked. */
|
|
42
|
+
render() {
|
|
43
|
+
return this.findings.map((f) => renderFindingLine(f)).join("\n");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=findings.js.map
|