@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,573 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scores coverage — M-ADR 0205 §4.5's coverage instrument, the fourteenth
|
|
3
|
+
* engine check (Phase B, B2). SQL on the §4.1 engine, like every other
|
|
4
|
+
* catalogue-reading predicate; `check-repo-config-coverage.sh` remains the
|
|
5
|
+
* only shell survivor and is not the model for this.
|
|
6
|
+
*
|
|
7
|
+
* The claim: *every eligible artefact carries a current score*. Eligibility is
|
|
8
|
+
* computed per rubric against the catalogue that holds its targets, and the
|
|
9
|
+
* predicate reports BOTH directions, because the two say different things:
|
|
10
|
+
*
|
|
11
|
+
* - an eligible target with no score is a GAP — nobody has judged it;
|
|
12
|
+
* - a score whose target does not resolve is an ORPHANED SCORE — the
|
|
13
|
+
* assessment corpus has decayed away from the model it describes.
|
|
14
|
+
*
|
|
15
|
+
* The second direction is not decoration. Measured on the forward estate at
|
|
16
|
+
* introduction: 85 of 99 ADR scores point at ids that no longer exist, because
|
|
17
|
+
* a rename moved the ADR canon and the registry keys were never migrated. A
|
|
18
|
+
* one-directional predicate would have reported that estate as having 99
|
|
19
|
+
* scored ADRs while none of them resolved.
|
|
20
|
+
*
|
|
21
|
+
* THE JOIN VOCABULARY IS THE WHOLE DIFFICULTY, and it is declared rather than
|
|
22
|
+
* guessed. A registry's entry keys are not the catalogue's ids, and the
|
|
23
|
+
* mismatch differs per rubric — measured, not assumed:
|
|
24
|
+
*
|
|
25
|
+
* - contract-quality keys are SPEC PATHS (`contracts-avro/.../X.avsc`),
|
|
26
|
+
* joining `contracts.spec_path`; against `contracts.id` they score 0 of 55.
|
|
27
|
+
* - adr-quality keys carry a tier prefix (`M-ADR-0001`) the catalogue strips
|
|
28
|
+
* (`ADR-0001`); the raw join scores 0 of 99.
|
|
29
|
+
* - domain-architecture keys are plain domain slugs and join `domains.id`
|
|
30
|
+
* directly, 13 of 13.
|
|
31
|
+
*
|
|
32
|
+
* So each rubric declares its catalogue, its join column and its reference
|
|
33
|
+
* transform, and the predicate REPORTS the resolution rate it achieved. A join
|
|
34
|
+
* that silently resolves nothing looks exactly like an estate that has scored
|
|
35
|
+
* nothing — the confident-false-zero shape this whole phase exists to remove.
|
|
36
|
+
*
|
|
37
|
+
* Rubrics whose targets have NO catalogue (c4-diagram, strategic-doc,
|
|
38
|
+
* user-story, work-order) have no eligibility denominator to compute. They are
|
|
39
|
+
* reported as such by name and their scores are still resolution-checked as
|
|
40
|
+
* far as they can be — never silently omitted, because a rubric missing from
|
|
41
|
+
* the output reads as a rubric with nothing to say.
|
|
42
|
+
*
|
|
43
|
+
* Reporting-only at both tiers, per §4.5. Exemption vocabulary starts EMPTY
|
|
44
|
+
* (M-ADR 0199): there is no way to mark a target exempt yet, deliberately, so
|
|
45
|
+
* the first figure is the honest total.
|
|
46
|
+
*
|
|
47
|
+
* ADOPTION GATING — a rubric is measured only where its REGISTRY EXISTS. An SDD
|
|
48
|
+
* that carries no `<rubric>-scores.yaml` has not adopted that rubric, so it has
|
|
49
|
+
* no eligible population for it, and reporting its whole ADR/contract/capability
|
|
50
|
+
* corpus as unscored would be true-but-useless noise. This is the same n/a rule
|
|
51
|
+
* the repo-config predicate applies and the same rule the quality-scores builder
|
|
52
|
+
* applies to an absent registry directory: absence of the whole surface is not a
|
|
53
|
+
* gap in it. Measured before adopting it: without this rule ONE reverse-estate
|
|
54
|
+
* SDD emits 630 gap lines for rubrics it has never used, which across that
|
|
55
|
+
* estate would swamp the baseline re-take and bury the real signal.
|
|
56
|
+
*
|
|
57
|
+
* The distinction that must NOT be lost, and is not: a registry that EXISTS but
|
|
58
|
+
* is EMPTY reports its full population as gaps. That is the method repo's own
|
|
59
|
+
* state — fifteen deliberately-empty containers — and it is exactly the silence
|
|
60
|
+
* §4.5 exists to make loud. Absent surface and empty surface are different
|
|
61
|
+
* claims, and conflating them is the defect this whole phase is about.
|
|
62
|
+
*
|
|
63
|
+
* The n/a count is always printed. A rubric skipped silently reads as a rubric
|
|
64
|
+
* with nothing to report.
|
|
65
|
+
*/
|
|
66
|
+
import { existsSync } from "node:fs";
|
|
67
|
+
import { join } from "node:path";
|
|
68
|
+
import { readLockFile } from "../lock-file.js";
|
|
69
|
+
/**
|
|
70
|
+
* The declared mapping. Three entries are EXERCISED against real estate data
|
|
71
|
+
* (adr-quality, contract-quality, domain-architecture); the rest are declared
|
|
72
|
+
* from the registry and rubric shapes and have no populated corpus to verify
|
|
73
|
+
* against yet — a distinction the metric line states rather than blurs.
|
|
74
|
+
*/
|
|
75
|
+
export const RUBRIC_ELIGIBILITY = [
|
|
76
|
+
// ── The banded reverse family (schema 3.3.0, Phase B4) ──────────────────
|
|
77
|
+
//
|
|
78
|
+
// EVERY join below was MEASURED against a real estate before being declared,
|
|
79
|
+
// and the three that do not resolve are declared unmeasurable rather than
|
|
80
|
+
// given a plausible transform. That discipline is not decorative: an earlier
|
|
81
|
+
// ADR mapping in this arc was "verified" by checking that its targets EXISTED
|
|
82
|
+
// in a dense id range, which is nearly free, and it turned out to point 75 of
|
|
83
|
+
// 99 scores at the wrong ADR. Existence is not correctness.
|
|
84
|
+
{
|
|
85
|
+
rubricId: "reverse-repo-quality.reverseability",
|
|
86
|
+
registryFile: "reverse-repo-quality-scores.yaml",
|
|
87
|
+
targetKind: "repository",
|
|
88
|
+
catalogue: "repositories",
|
|
89
|
+
joinColumn: "id",
|
|
90
|
+
refTransform: null,
|
|
91
|
+
eligibilityFilter: null,
|
|
92
|
+
eligibilityNote: "Every catalogued repository. Measured 123/123 resolving against 370 repositories — a real 33% coverage figure, not a join defect.",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
rubricId: "reverse-repo-quality.repo_doc_quality",
|
|
96
|
+
registryFile: "reverse-repo-quality-scores.yaml",
|
|
97
|
+
targetKind: "repository",
|
|
98
|
+
catalogue: "repositories",
|
|
99
|
+
joinColumn: "id",
|
|
100
|
+
refTransform: null,
|
|
101
|
+
eligibilityFilter: null,
|
|
102
|
+
eligibilityNote: "Same population as reverseability — the two rubrics judge different things about the same repository. Measured 123/123.",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
rubricId: "reverse-product-quality.product_synthesis_quality",
|
|
106
|
+
registryFile: "reverse-product-quality-scores.yaml",
|
|
107
|
+
targetKind: "product",
|
|
108
|
+
catalogue: "domains",
|
|
109
|
+
joinColumn: "id",
|
|
110
|
+
refTransform: null,
|
|
111
|
+
eligibilityFilter: null,
|
|
112
|
+
eligibilityNote: "Joins DOMAINS, not products, and that is measured rather than inferred from the name: 9/9 against domains, 0/9 against products. The reverse method's 'product' units are per-domain product areas; the target_kind records what the rubric calls them, the join records where they resolve.",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
rubricId: "reverse-platform-evidence-quality.reverse_platform_evidence_quality",
|
|
116
|
+
registryFile: "reverse-platform-evidence-quality-scores.yaml",
|
|
117
|
+
targetKind: "platform_evidence",
|
|
118
|
+
catalogue: "domains",
|
|
119
|
+
joinColumn: "id",
|
|
120
|
+
refTransform: null,
|
|
121
|
+
eligibilityFilter: null,
|
|
122
|
+
eligibilityNote: "Platform evidence is assessed per domain. Measured 9/9.",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
rubricId: "reverse-data-model-quality",
|
|
126
|
+
registryFile: "domain-artifact-scores.yaml",
|
|
127
|
+
targetKind: "data-model",
|
|
128
|
+
catalogue: "domain_models",
|
|
129
|
+
joinColumn: "id",
|
|
130
|
+
refTransform: null,
|
|
131
|
+
eligibilityFilter: null,
|
|
132
|
+
eligibilityNote: "Every domain model. Measured 9/9 against 12.",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
rubricId: "reverse-glossary-quality",
|
|
136
|
+
registryFile: "domain-artifact-scores.yaml",
|
|
137
|
+
targetKind: "glossary",
|
|
138
|
+
catalogue: "domains",
|
|
139
|
+
joinColumn: "id",
|
|
140
|
+
refTransform: (r) => `regexp_replace(${r}, '-glossary$', '')`,
|
|
141
|
+
eligibilityFilter: null,
|
|
142
|
+
eligibilityNote: "One glossary per domain; refs are '<domain>-glossary'. Measured 9/9 after the suffix strip, 0/9 without it.",
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
rubricId: "reverse-c4-quality",
|
|
146
|
+
registryFile: "domain-artifact-scores.yaml",
|
|
147
|
+
targetKind: "c4-l2",
|
|
148
|
+
catalogue: "domains",
|
|
149
|
+
joinColumn: "id",
|
|
150
|
+
refTransform: (r) => `regexp_replace(${r}, '-c4-l2$', '')`,
|
|
151
|
+
eligibilityFilter: null,
|
|
152
|
+
eligibilityNote: "One L2 diagram per domain; refs are '<domain>-c4-l2'. Measured 8/8 after the strip, 0/8 without it.",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
rubricId: "reverse-capability-spec-quality",
|
|
156
|
+
registryFile: "domain-artifact-scores.yaml",
|
|
157
|
+
targetKind: "capability-spec",
|
|
158
|
+
catalogue: "capabilities",
|
|
159
|
+
joinColumn: "id",
|
|
160
|
+
// Refs are '<domain>-capability-spec-<capability>'; the capability is the tail.
|
|
161
|
+
refTransform: (r) => `regexp_replace(${r}, '^.*-capability-spec-', '')`,
|
|
162
|
+
eligibilityFilter: "status <> 'deprecated'",
|
|
163
|
+
eligibilityNote: "Measured 19/19 on the tail. The domain prefix is NOT part of capabilities.id here, so the whole ref resolves to nothing (0/19) — the transform is required, not cosmetic. Excludes deprecated capabilities on the same footing as feature-grounding and traceability-manifest: three rubrics judging one catalogue must agree about who is in the population, and a rubric that disagreed would reintroduce as a metric discrepancy exactly the retirement the other two just retired. Missed on the first pass because the control hardcoded two rubric names instead of deriving them; it now derives them.",
|
|
164
|
+
},
|
|
165
|
+
// ── Banded rubrics with NO catalogued eligibility population ─────────────
|
|
166
|
+
{
|
|
167
|
+
rubricId: "reverse-feature-grounding-quality",
|
|
168
|
+
registryFile: "domain-artifact-scores.yaml",
|
|
169
|
+
targetKind: "feature-grounding",
|
|
170
|
+
catalogue: null,
|
|
171
|
+
joinColumn: "id",
|
|
172
|
+
refTransform: null,
|
|
173
|
+
eligibilityFilter: null,
|
|
174
|
+
eligibilityNote: "NO local population, measured rather than assumed. Refs are '<domain>-feature-grounding-<slug>' and the tails are SIBLING SDD SLUGS (app-shell, cold-chain, connect-rapide), not rows in this SDD: 0/15 against products, applications, features and repositories, and 1/15 against capabilities — a coincidence, not a join. A cross-SDD reference the member tier cannot resolve, so its coverage is declared unmeasurable rather than given a transform that would produce plausible wrong numbers.",
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
rubricId: "reverse-contract-quality",
|
|
178
|
+
registryFile: "domain-artifact-scores.yaml",
|
|
179
|
+
targetKind: "contract",
|
|
180
|
+
catalogue: null,
|
|
181
|
+
joinColumn: "id",
|
|
182
|
+
refTransform: null,
|
|
183
|
+
eligibilityFilter: null,
|
|
184
|
+
eligibilityNote: "HETEROGENEOUS targets in one rubric: '<domain>-event-catalog' (resolves to domains, 2/2) and '<domain>-openapi-<service>' (0/4 against contracts — the tails are service names, not contract ids). A rubric gets one eligibility entry, and half a join is worse than none, so the whole rubric is declared unmeasurable until the reverse method splits it.",
|
|
185
|
+
},
|
|
186
|
+
// ── The forward house form ───────────────────────────────────────────────
|
|
187
|
+
{
|
|
188
|
+
rubricId: "adr-quality",
|
|
189
|
+
targetKind: "adr",
|
|
190
|
+
catalogue: "adrs",
|
|
191
|
+
joinColumn: "id",
|
|
192
|
+
// The catalogue strips the tier prefix (M-ADR 0186 §4.4); registries keep it.
|
|
193
|
+
refTransform: (r) => `regexp_replace(${r}, '^[A-Z]+-ADR-', 'ADR-')`,
|
|
194
|
+
eligibilityFilter: "status = 'Accepted'",
|
|
195
|
+
managedPathColumn: "doc_path",
|
|
196
|
+
canonicalPathColumn: "doc_path",
|
|
197
|
+
eligibilityNote: "Accepted only, and adopter-owned only. A Proposed ADR is mid-decision and a Superseded one is history; requiring a current score on either would manufacture a gap nobody should close. The managed-path rule adds the second half of the same argument, ruled 2026-08-07: an adopter cannot remediate an ADR that sync overwrites, so a method-managed ADR is not in the adopter's denominator — its quality belongs to the producer's own registry, and an adopter's assessment of it is feedback to raise rather than a row to maintain. Measured at both estates 2026-08-08 (committed catalogues, member tier): forward 203 Accepted -> 83 adopter-owned (120 excluded); reverse 182 Accepted -> 62 adopter-owned (120 excluded). Zero rows under a producer path escaped the lock at either estate, so the lock and a path heuristic agree on today's data — the lock is used anyway, because agreement today is not the same as being the right rule.",
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
rubricId: "contract-quality",
|
|
201
|
+
targetKind: "contract",
|
|
202
|
+
catalogue: "contracts",
|
|
203
|
+
joinColumn: "spec_path",
|
|
204
|
+
refTransform: null,
|
|
205
|
+
eligibilityFilter: "status <> 'deprecated'",
|
|
206
|
+
canonicalPathColumn: "spec_path",
|
|
207
|
+
eligibilityNote: "Every non-deprecated contract. Deprecated contracts are excluded for the same reason the contract-coverage predicate excludes them — they are on the way out, and scoring them is work with no consumer.",
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
rubricId: "domain-architecture",
|
|
211
|
+
targetKind: "domain",
|
|
212
|
+
catalogue: "domains",
|
|
213
|
+
joinColumn: "id",
|
|
214
|
+
refTransform: null,
|
|
215
|
+
eligibilityFilter: null,
|
|
216
|
+
eligibilityNote: "Every domain on disk. A domain that exists is a domain that should be assessed.",
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
rubricId: "intent-quality",
|
|
220
|
+
targetKind: "domain",
|
|
221
|
+
catalogue: "domains",
|
|
222
|
+
joinColumn: "id",
|
|
223
|
+
refTransform: null,
|
|
224
|
+
eligibilityFilter: null,
|
|
225
|
+
eligibilityNote: "Every domain, same population as domain-architecture — the two rubrics judge different things about the same unit.",
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
rubricId: "feature-grounding",
|
|
229
|
+
targetKind: "capability",
|
|
230
|
+
catalogue: "capabilities",
|
|
231
|
+
joinColumn: "id",
|
|
232
|
+
refTransform: null,
|
|
233
|
+
eligibilityFilter: "status <> 'deprecated'",
|
|
234
|
+
eligibilityNote: "Every capability. EXCLUDES DEPRECATED CAPABILITIES — the SAME PATTERN contract-quality already applies to the contracts catalogue (identical operator and value, and its note gives the same reason: they are on the way out, and scoring them is work with no consumer), and the same argument adr-quality makes for a Superseded ADR. This applies an existing pattern to a third catalogue rather than setting a new one. This is the mechanism that retires a withdrawn capability's permanently-unanswerable findings, replacing the M-ADR 0199 exemption vocabulary that was investigated and NOT built: across both estates the authored-intent evidence base was zero, and a retirement expressed in the catalogue's OWN documented vocabulary needs no annotation surface. It resolves only where the retirement is DECLARED — a capability whose traceability manifest was deleted has no authored status and falls to the builder's hardcoded default, so declaring the state is the estate-side half.",
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
rubricId: "traceability-manifest",
|
|
238
|
+
targetKind: "capability",
|
|
239
|
+
catalogue: "capabilities",
|
|
240
|
+
joinColumn: "id",
|
|
241
|
+
refTransform: null,
|
|
242
|
+
eligibilityFilter: "status <> 'deprecated'",
|
|
243
|
+
eligibilityNote: "Every capability. Excludes deprecated capabilities per feature-grounding's note — the rubrics judging this catalogue must agree about who is in the population.",
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
rubricId: "glossary",
|
|
247
|
+
targetKind: "glossary",
|
|
248
|
+
catalogue: "domains",
|
|
249
|
+
joinColumn: "id",
|
|
250
|
+
refTransform: null,
|
|
251
|
+
eligibilityFilter: null,
|
|
252
|
+
eligibilityNote: "One glossary per domain — the registry keys by domain slug, so the eligible population is domains, not glossary_items (a glossary is scored whole, not term by term).",
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
rubricId: "persona",
|
|
256
|
+
targetKind: "persona",
|
|
257
|
+
catalogue: "personas",
|
|
258
|
+
joinColumn: "id",
|
|
259
|
+
refTransform: null,
|
|
260
|
+
eligibilityFilter: null,
|
|
261
|
+
eligibilityNote: "Every persona.",
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
rubricId: "product-definition",
|
|
265
|
+
targetKind: "product",
|
|
266
|
+
catalogue: "products",
|
|
267
|
+
joinColumn: "id",
|
|
268
|
+
refTransform: null,
|
|
269
|
+
eligibilityFilter: null,
|
|
270
|
+
eligibilityNote: "Every product.",
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
rubricId: "screen-spec",
|
|
274
|
+
targetKind: "screen",
|
|
275
|
+
catalogue: "screens",
|
|
276
|
+
joinColumn: "id",
|
|
277
|
+
refTransform: null,
|
|
278
|
+
eligibilityFilter: null,
|
|
279
|
+
eligibilityNote: "Every screen.",
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
rubricId: "integration-sdd-quality",
|
|
283
|
+
targetKind: "integration",
|
|
284
|
+
catalogue: "integrations",
|
|
285
|
+
joinColumn: "id",
|
|
286
|
+
refTransform: null,
|
|
287
|
+
eligibilityFilter: null,
|
|
288
|
+
eligibilityNote: "Every integration.",
|
|
289
|
+
},
|
|
290
|
+
// ── No catalogued eligibility population ────────────────────────────────
|
|
291
|
+
// These rubrics score artefacts the catalogue model does not hold, so there
|
|
292
|
+
// is no denominator to compute. Declared explicitly so they appear in the
|
|
293
|
+
// output as "no denominator" rather than being absent — an absent rubric
|
|
294
|
+
// reads as a rubric with nothing to report.
|
|
295
|
+
{
|
|
296
|
+
rubricId: "c4-diagram",
|
|
297
|
+
targetKind: "c4_diagram",
|
|
298
|
+
catalogue: null,
|
|
299
|
+
joinColumn: "id",
|
|
300
|
+
refTransform: null,
|
|
301
|
+
eligibilityFilter: null,
|
|
302
|
+
eligibilityNote: "Diagrams are files, not catalogue rows.",
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
rubricId: "strategic-doc",
|
|
306
|
+
targetKind: "strategic_doc",
|
|
307
|
+
catalogue: null,
|
|
308
|
+
joinColumn: "id",
|
|
309
|
+
refTransform: null,
|
|
310
|
+
eligibilityFilter: null,
|
|
311
|
+
eligibilityNote: "Strategic documents are not catalogued.",
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
rubricId: "user-story",
|
|
315
|
+
targetKind: "user_story",
|
|
316
|
+
catalogue: null,
|
|
317
|
+
joinColumn: "id",
|
|
318
|
+
refTransform: null,
|
|
319
|
+
eligibilityFilter: null,
|
|
320
|
+
eligibilityNote: "User stories are not catalogued.",
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
rubricId: "work-order",
|
|
324
|
+
targetKind: "work_order",
|
|
325
|
+
catalogue: null,
|
|
326
|
+
joinColumn: "id",
|
|
327
|
+
refTransform: null,
|
|
328
|
+
eligibilityFilter: null,
|
|
329
|
+
eligibilityNote: "Work orders are not catalogued.",
|
|
330
|
+
},
|
|
331
|
+
];
|
|
332
|
+
const sqlStr = (s) => `'${s.replace(/'/g, "''")}'`;
|
|
333
|
+
async function rows(session, sql) {
|
|
334
|
+
const reader = await session.conn.runAndReadAll(sql);
|
|
335
|
+
return reader.getRows();
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Every path claimed by a synced bundle in this repo's lock, mapped to the
|
|
339
|
+
* bundle claiming it.
|
|
340
|
+
*
|
|
341
|
+
* Empty at the producer — it carries no `method-baseline.lock`, so nothing is
|
|
342
|
+
* written from elsewhere and nothing is excluded. That is the degenerate case
|
|
343
|
+
* working by construction rather than by a repo-kind check: `canonical-method`
|
|
344
|
+
* is not even a member of the CLI's `RepoKind` union, so a check on it would
|
|
345
|
+
* have been a special case resting on a marker the type system does not model.
|
|
346
|
+
*/
|
|
347
|
+
async function managedPathOwners(sddRoot) {
|
|
348
|
+
const lock = await readLockFile(sddRoot);
|
|
349
|
+
const owners = new Map();
|
|
350
|
+
if (!lock)
|
|
351
|
+
return owners;
|
|
352
|
+
for (const bundle of lock.bundles) {
|
|
353
|
+
for (const p of bundle.managedPaths) {
|
|
354
|
+
// First claim wins, matching `pathToBundleMap`: a path claimed twice is a
|
|
355
|
+
// sync-side concern, and this predicate is not the place to adjudicate it.
|
|
356
|
+
if (!owners.has(p))
|
|
357
|
+
owners.set(p, bundle.name);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return owners;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Runs the predicate. `available` names the catalogues actually present, so a
|
|
364
|
+
* rubric whose population file is absent is skipped the way every other
|
|
365
|
+
* predicate skips an absent population — silently, because absence of the
|
|
366
|
+
* whole surface is not a gap in it — while a rubric whose population is
|
|
367
|
+
* PRESENT but empty still reports its zero.
|
|
368
|
+
*/
|
|
369
|
+
export async function runScoresCoverage(session, options) {
|
|
370
|
+
const findings = [];
|
|
371
|
+
const metrics = [];
|
|
372
|
+
if (!options.available.has("quality_scores")) {
|
|
373
|
+
return {
|
|
374
|
+
findings: [],
|
|
375
|
+
metrics: [],
|
|
376
|
+
undeclaredRubrics: [],
|
|
377
|
+
notAdopted: [],
|
|
378
|
+
stalenessBasis: { corpusRef: 0, scoredAt: 0, none: 0 },
|
|
379
|
+
totalScores: 0,
|
|
380
|
+
transformBridgedRefs: 0,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
const totalScores = Number((await rows(session, `SELECT COUNT(*) FROM "quality_scores";`))[0][0]);
|
|
384
|
+
const transformBridgedRefs = Number((await rows(session, `SELECT COUNT(*) FROM "quality_scores"
|
|
385
|
+
WHERE rubric_id = 'adr-quality' AND regexp_matches(target_ref, '^[A-Z]+-ADR-');`))[0][0]);
|
|
386
|
+
const basis = await rows(session, `SELECT staleness_basis, COUNT(*) FROM "quality_scores" GROUP BY 1 ORDER BY 1;`);
|
|
387
|
+
const stalenessBasis = { corpusRef: 0, scoredAt: 0, none: 0 };
|
|
388
|
+
for (const [b, n] of basis) {
|
|
389
|
+
const c = Number(n);
|
|
390
|
+
if (b === "corpus_ref")
|
|
391
|
+
stalenessBasis.corpusRef = c;
|
|
392
|
+
else if (b === "scored_at")
|
|
393
|
+
stalenessBasis.scoredAt = c;
|
|
394
|
+
else
|
|
395
|
+
stalenessBasis.none = c;
|
|
396
|
+
}
|
|
397
|
+
// A row that can support no staleness verdict at all is its own finding.
|
|
398
|
+
// Without it the predicate would silently treat "unknown" as "fresh", which
|
|
399
|
+
// is the basis error this column exists to prevent.
|
|
400
|
+
if (stalenessBasis.none > 0) {
|
|
401
|
+
findings.push({
|
|
402
|
+
kind: "unresolvable-staleness",
|
|
403
|
+
rubricId: "(all)",
|
|
404
|
+
subject: `${stalenessBasis.none} score(s) carry neither corpus_ref nor a resolvable target_path — their currency is UNKNOWN, not fresh`,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
const managedOwners = await managedPathOwners(options.sddRoot);
|
|
408
|
+
const declared = new Set(RUBRIC_ELIGIBILITY.map((r) => r.rubricId));
|
|
409
|
+
const present = await rows(session, `SELECT DISTINCT rubric_id FROM "quality_scores" ORDER BY 1;`);
|
|
410
|
+
const undeclaredRubrics = present
|
|
411
|
+
.map((r) => String(r[0]))
|
|
412
|
+
.filter((r) => !declared.has(r));
|
|
413
|
+
const adopted = (spec) => existsSync(join(options.sddRoot, "orchestration", "gate-registry", spec.registryFile ?? `${spec.rubricId}-scores.yaml`));
|
|
414
|
+
const notAdopted = [];
|
|
415
|
+
for (const spec of RUBRIC_ELIGIBILITY) {
|
|
416
|
+
if (!adopted(spec)) {
|
|
417
|
+
notAdopted.push(spec.rubricId);
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
const scored = Number((await rows(session, `SELECT COUNT(*) FROM "quality_scores" WHERE rubric_id = ${sqlStr(spec.rubricId)};`))[0][0]);
|
|
421
|
+
// No catalogued population: report the scores, state that there is no
|
|
422
|
+
// denominator, and do not invent one.
|
|
423
|
+
if (spec.catalogue === null) {
|
|
424
|
+
if (scored > 0) {
|
|
425
|
+
metrics.push({
|
|
426
|
+
rubricId: spec.rubricId,
|
|
427
|
+
targetKind: spec.targetKind,
|
|
428
|
+
eligible: null,
|
|
429
|
+
covered: scored,
|
|
430
|
+
scored,
|
|
431
|
+
orphaned: 0,
|
|
432
|
+
managedExcluded: 0,
|
|
433
|
+
managedExcludedByBundle: {},
|
|
434
|
+
recordedPath: null,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (!options.available.has(spec.catalogue))
|
|
440
|
+
continue;
|
|
441
|
+
// The method-managed exclusion, resolved BEFORE any count is taken so that
|
|
442
|
+
// every figure below shares one denominator. Computed here rather than
|
|
443
|
+
// inlined at each query because `eligibleWhere` reaches four call sites,
|
|
444
|
+
// and a rule applied to three of them is worse than a rule applied to none.
|
|
445
|
+
const excludedRows = spec.managedPathColumn && managedOwners.size > 0
|
|
446
|
+
? await rows(session, `SELECT "${spec.managedPathColumn}" FROM "${spec.catalogue}"
|
|
447
|
+
WHERE (${spec.eligibilityFilter ?? "TRUE"})
|
|
448
|
+
AND "${spec.managedPathColumn}" IN (${[...managedOwners.keys()].map(sqlStr).join(", ")});`)
|
|
449
|
+
: [];
|
|
450
|
+
const managedExcludedByBundle = {};
|
|
451
|
+
for (const [p] of excludedRows) {
|
|
452
|
+
const bundle = managedOwners.get(String(p)) ?? "(unknown bundle)";
|
|
453
|
+
managedExcludedByBundle[bundle] = (managedExcludedByBundle[bundle] ?? 0) + 1;
|
|
454
|
+
}
|
|
455
|
+
const managedExcluded = excludedRows.length;
|
|
456
|
+
const eligibleWhere = managedExcluded > 0
|
|
457
|
+
? `(${spec.eligibilityFilter ?? "TRUE"}) AND "${spec.managedPathColumn}" NOT IN (${[
|
|
458
|
+
...managedOwners.keys(),
|
|
459
|
+
]
|
|
460
|
+
.map(sqlStr)
|
|
461
|
+
.join(", ")})`
|
|
462
|
+
: (spec.eligibilityFilter ?? "TRUE");
|
|
463
|
+
if (scored === 0) {
|
|
464
|
+
// The population exists and nothing is scored — the loudest case, and
|
|
465
|
+
// exactly the silence M-ADR 0205 §4.5 names. Every eligible row is a gap.
|
|
466
|
+
const eligibleRows = await rows(session, `SELECT id FROM "${spec.catalogue}" WHERE (${eligibleWhere}) ORDER BY id;`);
|
|
467
|
+
for (const [id] of eligibleRows) {
|
|
468
|
+
findings.push({ kind: "uncovered", rubricId: spec.rubricId, subject: String(id) });
|
|
469
|
+
}
|
|
470
|
+
metrics.push({
|
|
471
|
+
rubricId: spec.rubricId,
|
|
472
|
+
targetKind: spec.targetKind,
|
|
473
|
+
eligible: eligibleRows.length,
|
|
474
|
+
covered: 0,
|
|
475
|
+
scored: 0,
|
|
476
|
+
orphaned: 0,
|
|
477
|
+
managedExcluded,
|
|
478
|
+
managedExcludedByBundle,
|
|
479
|
+
recordedPath: spec.canonicalPathColumn !== undefined ? { agree: 0, mismatch: 0, noRecordedPath: 0 } : null,
|
|
480
|
+
});
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
const ref = spec.refTransform ? spec.refTransform("s.target_ref") : "s.target_ref";
|
|
484
|
+
// Eligibility is always a conjunct, so it composes with the not-exists
|
|
485
|
+
// clause without any string surgery on an assembled WHERE — `TRUE` for the
|
|
486
|
+
// unfiltered case keeps one query shape instead of two.
|
|
487
|
+
const scoreSet = `SELECT * FROM "quality_scores" WHERE rubric_id = ${sqlStr(spec.rubricId)}`;
|
|
488
|
+
// Direction 1 — eligible targets with no score.
|
|
489
|
+
const uncovered = await rows(session, `SELECT c.id FROM "${spec.catalogue}" c
|
|
490
|
+
WHERE (${eligibleWhere})
|
|
491
|
+
AND NOT EXISTS (SELECT 1 FROM (${scoreSet}) s WHERE ${ref} = c."${spec.joinColumn}")
|
|
492
|
+
ORDER BY c.id;`);
|
|
493
|
+
for (const [id] of uncovered) {
|
|
494
|
+
findings.push({ kind: "uncovered", rubricId: spec.rubricId, subject: String(id) });
|
|
495
|
+
}
|
|
496
|
+
// Direction 2 — scores whose target does not resolve.
|
|
497
|
+
const orphaned = await rows(session, `SELECT s.target_ref FROM (${scoreSet}) s
|
|
498
|
+
WHERE NOT EXISTS (
|
|
499
|
+
SELECT 1 FROM "${spec.catalogue}" c WHERE c."${spec.joinColumn}" = ${ref}
|
|
500
|
+
)
|
|
501
|
+
ORDER BY s.target_ref;`);
|
|
502
|
+
for (const [r] of orphaned) {
|
|
503
|
+
findings.push({ kind: "orphaned-score", rubricId: spec.rubricId, subject: String(r) });
|
|
504
|
+
}
|
|
505
|
+
// Direction 3 — scores whose target RESOLVES but sits outside this repo's
|
|
506
|
+
// scope. Only reachable where the managed rule actually excluded something.
|
|
507
|
+
if (managedExcluded > 0) {
|
|
508
|
+
const outOfScope = await rows(session, `SELECT s.target_ref FROM (${scoreSet}) s
|
|
509
|
+
WHERE EXISTS (
|
|
510
|
+
SELECT 1 FROM "${spec.catalogue}" c
|
|
511
|
+
WHERE c."${spec.joinColumn}" = ${ref}
|
|
512
|
+
AND c."${spec.managedPathColumn}" IN (${[...managedOwners.keys()]
|
|
513
|
+
.map(sqlStr)
|
|
514
|
+
.join(", ")})
|
|
515
|
+
)
|
|
516
|
+
ORDER BY s.target_ref;`);
|
|
517
|
+
for (const [r] of outOfScope) {
|
|
518
|
+
findings.push({
|
|
519
|
+
kind: "out-of-scope-score",
|
|
520
|
+
rubricId: spec.rubricId,
|
|
521
|
+
subject: String(r),
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
// Direction 4 — a resolved score whose RECORDED path is not the target's
|
|
526
|
+
// canonical path (decision #3, report-only). Paths are normalised on both
|
|
527
|
+
// sides (leading `./` stripped, backslashes to `/`) before comparison. A
|
|
528
|
+
// path the builder filled in from the catalogue agrees by construction —
|
|
529
|
+
// the metric says "agree", not "verified".
|
|
530
|
+
let recordedPath = null;
|
|
531
|
+
if (spec.canonicalPathColumn !== undefined) {
|
|
532
|
+
const pairs = await rows(session, `SELECT s.target_ref, s.target_path, c."${spec.canonicalPathColumn}"
|
|
533
|
+
FROM (${scoreSet}) s
|
|
534
|
+
JOIN "${spec.catalogue}" c ON c."${spec.joinColumn}" = ${ref}
|
|
535
|
+
ORDER BY s.target_ref;`);
|
|
536
|
+
let agree = 0;
|
|
537
|
+
let mismatch = 0;
|
|
538
|
+
let noRecordedPath = 0;
|
|
539
|
+
const n = (v) => String(v ?? "").replace(/\\/g, "/").replace(/^(\.\/)+/, "");
|
|
540
|
+
for (const [targetRef, recorded, canonical] of pairs) {
|
|
541
|
+
const r = n(recorded);
|
|
542
|
+
const c = n(canonical);
|
|
543
|
+
if (r === "")
|
|
544
|
+
noRecordedPath++;
|
|
545
|
+
else if (c === "" || r === c)
|
|
546
|
+
agree++;
|
|
547
|
+
else {
|
|
548
|
+
mismatch++;
|
|
549
|
+
findings.push({
|
|
550
|
+
kind: "recorded-path-mismatch",
|
|
551
|
+
rubricId: spec.rubricId,
|
|
552
|
+
subject: `${String(targetRef)} — recorded "${r}", catalogue "${c}"`,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
recordedPath = { agree, mismatch, noRecordedPath };
|
|
557
|
+
}
|
|
558
|
+
const eligible = Number((await rows(session, `SELECT COUNT(*) FROM "${spec.catalogue}" WHERE (${eligibleWhere});`))[0][0]);
|
|
559
|
+
metrics.push({
|
|
560
|
+
rubricId: spec.rubricId,
|
|
561
|
+
targetKind: spec.targetKind,
|
|
562
|
+
eligible,
|
|
563
|
+
covered: eligible - uncovered.length,
|
|
564
|
+
scored,
|
|
565
|
+
orphaned: orphaned.length,
|
|
566
|
+
managedExcluded,
|
|
567
|
+
managedExcludedByBundle,
|
|
568
|
+
recordedPath,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
return { findings, metrics, undeclaredRubrics, notAdopted, stalenessBasis, totalScores, transformBridgedRefs };
|
|
572
|
+
}
|
|
573
|
+
//# sourceMappingURL=scores-coverage.js.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loader for the optional `.sdd/screen-validation.yaml` config — the
|
|
3
|
+
* adopter-tunable parameters for screen-spec composition-depth validation
|
|
4
|
+
* (ADR 0168 §4.5) and the archetype overlay (ADR 0169 §4.3).
|
|
5
|
+
*
|
|
6
|
+
* Every field is optional and carries a method default; an absent file means
|
|
7
|
+
* "method defaults only" (the vocabulary-free checks all still run). Follows
|
|
8
|
+
* the same shape convention as the `.sdd/*-aliases.yaml` loaders.
|
|
9
|
+
*
|
|
10
|
+
* File shape:
|
|
11
|
+
*
|
|
12
|
+
* flat_threshold: 6 # screen-flat-composition N (default 6)
|
|
13
|
+
* decorative_primitives: # unlocks screen-styling-primitive-as-structure
|
|
14
|
+
* - Spacer
|
|
15
|
+
* - Divider
|
|
16
|
+
* archetype_overlays: # per-platform overlay (ADR 0169 §4.3)
|
|
17
|
+
* web-generic:
|
|
18
|
+
* strip_prefixes: [Mdc, Mde] # render-only: stripped when humanising labels
|
|
19
|
+
* overrides:
|
|
20
|
+
* VBespokeGrid: grid
|
|
21
|
+
* swiftui:
|
|
22
|
+
* overrides:
|
|
23
|
+
* BrandCard: card
|
|
24
|
+
*
|
|
25
|
+
* The decorative set + the overlays are adopter-owned (vendor-standard defaults
|
|
26
|
+
* ship in the method; bespoke names live here). The checks themselves stay
|
|
27
|
+
* vocabulary-free — they consume these parameters, they don't hardcode them.
|
|
28
|
+
*/
|
|
29
|
+
import { readFile } from "node:fs/promises";
|
|
30
|
+
import { join } from "node:path";
|
|
31
|
+
import { parse as parseYaml } from "yaml";
|
|
32
|
+
export const SCREEN_VALIDATION_REL_PATH = join(".sdd", "screen-validation.yaml");
|
|
33
|
+
/**
|
|
34
|
+
* Load the optional screen-validation config. Returns `null` when the file is
|
|
35
|
+
* absent — callers apply method defaults. Throws on malformed YAML.
|
|
36
|
+
*/
|
|
37
|
+
export async function loadScreenValidationConfig(sddPath) {
|
|
38
|
+
const full = join(sddPath, SCREEN_VALIDATION_REL_PATH);
|
|
39
|
+
let text;
|
|
40
|
+
try {
|
|
41
|
+
text = await readFile(full, "utf-8");
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
if (err.code === "ENOENT")
|
|
45
|
+
return null;
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
const raw = parseYaml(text);
|
|
49
|
+
if (raw === null || typeof raw !== "object") {
|
|
50
|
+
throw new Error(`${SCREEN_VALIDATION_REL_PATH}: expected a mapping at the document root`);
|
|
51
|
+
}
|
|
52
|
+
const flatThreshold = typeof raw.flat_threshold === "number" && Number.isFinite(raw.flat_threshold) && raw.flat_threshold > 0
|
|
53
|
+
? raw.flat_threshold
|
|
54
|
+
: undefined;
|
|
55
|
+
const decorativePrimitives = new Set();
|
|
56
|
+
if (Array.isArray(raw.decorative_primitives)) {
|
|
57
|
+
for (const v of raw.decorative_primitives) {
|
|
58
|
+
if (typeof v === "string" && v !== "")
|
|
59
|
+
decorativePrimitives.add(v);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const archetypeOverlays = new Map();
|
|
63
|
+
if (raw.archetype_overlays !== null && typeof raw.archetype_overlays === "object" && !Array.isArray(raw.archetype_overlays)) {
|
|
64
|
+
for (const [platform, body] of Object.entries(raw.archetype_overlays)) {
|
|
65
|
+
const overrides = toStringMap(body?.overrides);
|
|
66
|
+
const stripPrefixes = toStringList(body?.strip_prefixes);
|
|
67
|
+
if (Object.keys(overrides).length > 0 || stripPrefixes.length > 0) {
|
|
68
|
+
archetypeOverlays.set(platform, { overrides, strip_prefixes: stripPrefixes });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { flatThreshold, decorativePrimitives, archetypeOverlays, relPath: SCREEN_VALIDATION_REL_PATH };
|
|
73
|
+
}
|
|
74
|
+
function toStringMap(raw) {
|
|
75
|
+
const out = {};
|
|
76
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
77
|
+
return out;
|
|
78
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
79
|
+
if (typeof v === "string" && v !== "")
|
|
80
|
+
out[k] = v;
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
function toStringList(raw) {
|
|
85
|
+
if (!Array.isArray(raw))
|
|
86
|
+
return [];
|
|
87
|
+
return raw.filter((v) => typeof v === "string" && v !== "");
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=screen-validation-loader.js.map
|