@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,169 @@
|
|
|
1
|
+
// `sdd-cli snapshot targets` — route each delta.yaml finding to the SDD
|
|
2
|
+
// artefacts it grounds, recording the GRAIN each route was found at:
|
|
3
|
+
//
|
|
4
|
+
// file — the artefact cites a changed file's path (feature-grounding style
|
|
5
|
+
// evidence citing `src/…`): the route is as precise as the citation;
|
|
6
|
+
// repo — the artefact only names the member's canonical_repo_key: the route
|
|
7
|
+
// reaches everything that repo grounds, and must not read as precise.
|
|
8
|
+
//
|
|
9
|
+
// Findings whose targets are already set are left alone (routing refined by
|
|
10
|
+
// hand is judgement). Findings nothing cites are listed as unroutable, never
|
|
11
|
+
// dropped. Writes back into delta.yaml preserving everything else in it.
|
|
12
|
+
import { readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
13
|
+
import { join, relative, sep } from "node:path";
|
|
14
|
+
import { parseDocument } from "yaml";
|
|
15
|
+
import { deltaPath } from "./diff.js";
|
|
16
|
+
import { SnapshotError, exists } from "./manifest.js";
|
|
17
|
+
/** Minimal glob → RegExp: `**` any path, `*` within a segment, `?` one character. */
|
|
18
|
+
export function globToRegExp(glob) {
|
|
19
|
+
let re = "";
|
|
20
|
+
for (let i = 0; i < glob.length; i++) {
|
|
21
|
+
const c = glob[i];
|
|
22
|
+
if (c === "*" && glob[i + 1] === "*") {
|
|
23
|
+
re += glob[i + 2] === "/" ? "(?:.*/)?" : ".*";
|
|
24
|
+
i += glob[i + 2] === "/" ? 2 : 1;
|
|
25
|
+
}
|
|
26
|
+
else if (c === "*")
|
|
27
|
+
re += "[^/]*";
|
|
28
|
+
else if (c === "?")
|
|
29
|
+
re += "[^/]";
|
|
30
|
+
else
|
|
31
|
+
re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
32
|
+
}
|
|
33
|
+
return new RegExp(`^${re}$`);
|
|
34
|
+
}
|
|
35
|
+
const TEXT = /\.(md|ya?ml|json|txt|adoc|mermaid|puml|csv)$/i;
|
|
36
|
+
const SKIP_DIRS = new Set([".git", "node_modules", ".sdd", "catalogues"]);
|
|
37
|
+
const MAX_BYTES = 4 * 1024 * 1024;
|
|
38
|
+
/** Every text artefact of the SDD, excluding the method's own copies and generated catalogues. */
|
|
39
|
+
export async function sddArtefacts(sddPath) {
|
|
40
|
+
const out = new Map();
|
|
41
|
+
async function walk(dir) {
|
|
42
|
+
for (const e of await readdir(dir, { withFileTypes: true })) {
|
|
43
|
+
const full = join(dir, e.name);
|
|
44
|
+
const rel = relative(sddPath, full).split(sep).join("/");
|
|
45
|
+
if (e.isDirectory()) {
|
|
46
|
+
if (SKIP_DIRS.has(e.name) || rel === "docs/method")
|
|
47
|
+
continue;
|
|
48
|
+
await walk(full);
|
|
49
|
+
}
|
|
50
|
+
else if (e.isFile() && TEXT.test(e.name) && (await stat(full)).size <= MAX_BYTES) {
|
|
51
|
+
out.set(rel, await readFile(full, "utf8"));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
await walk(sddPath);
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
// A path needs two segments to be a citation rather than a coincidence:
|
|
59
|
+
// `pom.xml` alone appears in every Java SDD.
|
|
60
|
+
const citable = (p) => p.includes("/");
|
|
61
|
+
/**
|
|
62
|
+
* `shared` holds paths that more than one member changed (`helm/values.yaml`,
|
|
63
|
+
* `src/main/resources/application.properties`): a citation of one of those names
|
|
64
|
+
* nobody in particular, so it is file grain only on a line that also names this
|
|
65
|
+
* member. Without that, a citation of another member's copy reads as precise.
|
|
66
|
+
*/
|
|
67
|
+
export function routeFinding(f, artefacts, shared = new Set()) {
|
|
68
|
+
const targets = new Map();
|
|
69
|
+
const paths = [...f.files.added, ...f.files.removed, ...f.files.modified].filter(citable);
|
|
70
|
+
// The key alone, or in export-folder form (`<key>-master@<rev>`): a hyphen may
|
|
71
|
+
// follow it only as a branch suffix, so `tcu` never matches inside `tcu-collector`.
|
|
72
|
+
const keyRe = new RegExp(`(^|[^A-Za-z0-9_-])${f.member.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(-(master|main|develop|trunk)@|[^A-Za-z0-9_-]|$)`);
|
|
73
|
+
const cites = (text, p) => shared.has(p) ? text.split("\n").some((line) => line.includes(p) && keyRe.test(line)) : text.includes(p);
|
|
74
|
+
// Folder-qualified — `<key>-<branch>@<rev>/<path>` — names the member AND the file,
|
|
75
|
+
// so it routes at file grain even for a one-segment path (`…@f504/build.gradle`).
|
|
76
|
+
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
77
|
+
const all = [...f.files.added, ...f.files.removed, ...f.files.modified];
|
|
78
|
+
const qualified = (text, p) => new RegExp(`${esc(f.member)}(-(master|main|develop|trunk))?@[0-9a-f]+/${esc(p)}(?![\\w/.-])`).test(text);
|
|
79
|
+
for (const [rel, text] of artefacts) {
|
|
80
|
+
const hit = paths.find((p) => cites(text, p)) ?? all.find((p) => qualified(text, p));
|
|
81
|
+
if (hit)
|
|
82
|
+
targets.set(rel, { artefact: rel, grain: "file", cited: hit });
|
|
83
|
+
}
|
|
84
|
+
for (const [rel, text] of artefacts) {
|
|
85
|
+
if (!targets.has(rel) && keyRe.test(text))
|
|
86
|
+
targets.set(rel, { artefact: rel, grain: "repo", cited: f.member });
|
|
87
|
+
}
|
|
88
|
+
return [...targets.values()].sort((a, b) => (a.grain === b.grain ? a.artefact.localeCompare(b.artefact) : a.grain === "file" ? -1 : 1));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Artefacts that mention a changed file by BASENAME only (`CollectorActor.java:49`) —
|
|
92
|
+
* too ambiguous to route, too likely to matter to drop silently. Reported, not routed.
|
|
93
|
+
*/
|
|
94
|
+
export function basenameOnly(f, artefacts, routed) {
|
|
95
|
+
const names = [...new Set([...f.files.added, ...f.files.removed, ...f.files.modified].map((p) => p.split("/").pop()))]
|
|
96
|
+
.filter((b) => /\.[A-Za-z0-9]+$/.test(b) && b.length >= 8);
|
|
97
|
+
const out = [];
|
|
98
|
+
for (const [rel, text] of artefacts) {
|
|
99
|
+
if (!routed.has(rel) && names.some((b) => new RegExp(`(^|[^\\w/.-])${b.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w.-])`).test(text)))
|
|
100
|
+
out.push(rel);
|
|
101
|
+
}
|
|
102
|
+
return out.sort();
|
|
103
|
+
}
|
|
104
|
+
export async function runSnapshotTargets(inputs, write) {
|
|
105
|
+
const dp = deltaPath(inputs.areaRoot, inputs.id);
|
|
106
|
+
if (!(await exists(dp))) {
|
|
107
|
+
write(`REFUSED: no ${dp} — run snapshot diff first\n`);
|
|
108
|
+
return { exitCode: 1 };
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const doc = parseDocument(await readFile(dp, "utf8"));
|
|
112
|
+
const findings = doc.toJS().findings ?? [];
|
|
113
|
+
const derivedRes = (inputs.derived ?? []).map(globToRegExp);
|
|
114
|
+
const kindOf = (artefact) => (derivedRes.some((r) => r.test(artefact)) ? "derived" : "spec");
|
|
115
|
+
const artefacts = await sddArtefacts(inputs.sddPath);
|
|
116
|
+
const owners = new Map();
|
|
117
|
+
for (const f of findings) {
|
|
118
|
+
for (const p of [...(f.files?.added ?? []), ...(f.files?.removed ?? []), ...(f.files?.modified ?? [])]) {
|
|
119
|
+
if (!owners.has(p))
|
|
120
|
+
owners.set(p, new Set());
|
|
121
|
+
owners.get(p).add(f.member);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const shared = new Set([...owners].filter(([, m]) => m.size > 1).map(([p]) => p));
|
|
125
|
+
let routed = 0;
|
|
126
|
+
let kept = 0;
|
|
127
|
+
const unroutable = [];
|
|
128
|
+
const grains = { file: 0, repo: 0, confirmed: 0 };
|
|
129
|
+
const kinds = { spec: 0, derived: 0 };
|
|
130
|
+
for (const [i, f] of findings.entries()) {
|
|
131
|
+
if (Array.isArray(f.targets) && f.targets.length > 0) {
|
|
132
|
+
kept++;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const t = routeFinding(f, artefacts, shared);
|
|
136
|
+
const loose = basenameOnly(f, artefacts, new Set(t.filter((x) => x.grain === "file").map((x) => x.artefact)));
|
|
137
|
+
if (loose.length > 0)
|
|
138
|
+
write(`${f.id}: ${loose.length} artefact(s) mention a changed file by basename only, not routed at file grain: ${loose.slice(0, 4).join(", ")}${loose.length > 4 ? ", …" : ""}\n`);
|
|
139
|
+
if (t.length === 0) {
|
|
140
|
+
unroutable.push(f.id);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
for (const x of t) {
|
|
144
|
+
x.kind = kindOf(x.artefact);
|
|
145
|
+
grains[x.grain]++;
|
|
146
|
+
kinds[x.kind]++;
|
|
147
|
+
}
|
|
148
|
+
doc.setIn(["findings", i, "targets"], doc.createNode(t));
|
|
149
|
+
routed++;
|
|
150
|
+
}
|
|
151
|
+
await writeFile(dp, doc.toString({ lineWidth: 0 }));
|
|
152
|
+
write(`${routed} finding(s) routed (${grains.file} file-grain, ${grains.repo} repo-grain target(s)); ${kept} already targeted, left alone; ` +
|
|
153
|
+
`${unroutable.length} unroutable${unroutable.length ? `: ${unroutable.join(", ")}` : ""}\n` +
|
|
154
|
+
`targets by kind: ${kinds.spec} spec (re-derive by reading), ${kinds.derived} derived (regenerate)` +
|
|
155
|
+
`${(inputs.derived ?? []).length === 0 ? " — no --derived globs given, so every target is counted as a spec" : ""}\n`);
|
|
156
|
+
if (grains.repo > 0) {
|
|
157
|
+
write("note: a repo-grain target reaches everything that repo grounds — narrow it by hand before re-reversing\n");
|
|
158
|
+
}
|
|
159
|
+
return { exitCode: 0 };
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
if (e instanceof SnapshotError) {
|
|
163
|
+
write(`REFUSED: ${e.message}\n`);
|
|
164
|
+
return { exitCode: 1 };
|
|
165
|
+
}
|
|
166
|
+
throw e;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=targets.js.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// `sdd-cli snapshot verify` — T0 provenance before the diff is trusted
|
|
2
|
+
// (delta-reverse.md §3).
|
|
3
|
+
//
|
|
4
|
+
// Two legs. LOCAL, checked here: each member folder exists, its `@revision`
|
|
5
|
+
// suffix matches source_revision, and where the export kept its `.git`, HEAD is
|
|
6
|
+
// that revision. CANONICAL — that the revision is a real commit on the canonical
|
|
7
|
+
// repo — cannot be proven from an export, so it is supplied: `--confirmed
|
|
8
|
+
// key=<how it was checked>` per member, or one recorded `--waiver` (who, why,
|
|
9
|
+
// date). A member with neither is "cannot assert", which FAILS: could-not-check
|
|
10
|
+
// is never silently checked. The result is written beside the baseline as
|
|
11
|
+
// provenance.yaml, so the waiver travels with the baseline it excuses.
|
|
12
|
+
import { execFile as execFileCb } from "node:child_process";
|
|
13
|
+
import { writeFile } from "node:fs/promises";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { promisify } from "node:util";
|
|
16
|
+
import { stringify } from "yaml";
|
|
17
|
+
import { SnapshotError, credentialNotice, exists, isIsoDate, memberBytesDir, readManifest, splitFolder } from "./manifest.js";
|
|
18
|
+
const execFile = promisify(execFileCb);
|
|
19
|
+
export async function runSnapshotVerify(inputs, write) {
|
|
20
|
+
try {
|
|
21
|
+
const m = await readManifest(inputs.areaRoot, inputs.id);
|
|
22
|
+
const notice = credentialNotice(m);
|
|
23
|
+
if (notice)
|
|
24
|
+
write(`${notice}\n`);
|
|
25
|
+
const w = inputs.waiver;
|
|
26
|
+
if (w && (!w.who || !w.why || !isIsoDate(w.date))) {
|
|
27
|
+
throw new SnapshotError("--waiver needs who=…,why=…,date=YYYY-MM-DD — an unattributed waiver is a skip with a name on it");
|
|
28
|
+
}
|
|
29
|
+
// A snapshot with no members has no provenance to assert. Refused before
|
|
30
|
+
// anything is written: `pass: 0 member(s)` used to be persisted to
|
|
31
|
+
// provenance.yaml as `result: pass` (empty-set sweep, R2, ruled 2026-09-29).
|
|
32
|
+
if (m.members.length === 0) {
|
|
33
|
+
throw new SnapshotError(`${inputs.id} has 0 members — nothing to verify, and an empty verification is not a pass`);
|
|
34
|
+
}
|
|
35
|
+
const rows = [];
|
|
36
|
+
const failures = [];
|
|
37
|
+
for (const mem of m.members) {
|
|
38
|
+
const dir = memberBytesDir(inputs.areaRoot, m, mem);
|
|
39
|
+
let local = "ok";
|
|
40
|
+
if (!(await exists(dir)))
|
|
41
|
+
local = `FAIL: ${dir} does not exist`;
|
|
42
|
+
else if (splitFolder(mem.exported_folder_name)?.revision !== mem.source_revision) {
|
|
43
|
+
local = `FAIL: folder revision does not match source_revision ${mem.source_revision}`;
|
|
44
|
+
}
|
|
45
|
+
else if (await exists(join(dir, ".git"))) {
|
|
46
|
+
const head = (await execFile("git", ["-C", dir, "rev-parse", "HEAD"])).stdout.trim();
|
|
47
|
+
if (!head.startsWith(mem.source_revision))
|
|
48
|
+
local = `FAIL: .git HEAD ${head} is not ${mem.source_revision}`;
|
|
49
|
+
else
|
|
50
|
+
local = "ok (git HEAD matches)";
|
|
51
|
+
}
|
|
52
|
+
const how = inputs.confirmed?.get(mem.canonical_repo_key);
|
|
53
|
+
const canonical = how ? `confirmed: ${how}` : w ? "waived" : "CANNOT ASSERT";
|
|
54
|
+
if (local.startsWith("FAIL"))
|
|
55
|
+
failures.push(`${mem.canonical_repo_key}: ${local}`);
|
|
56
|
+
if (canonical === "CANNOT ASSERT")
|
|
57
|
+
failures.push(`${mem.canonical_repo_key}: canonical provenance not confirmed and no waiver`);
|
|
58
|
+
rows.push({ canonical_repo_key: mem.canonical_repo_key, local, canonical });
|
|
59
|
+
}
|
|
60
|
+
for (const k of inputs.confirmed?.keys() ?? []) {
|
|
61
|
+
if (!m.members.some((x) => x.canonical_repo_key === k))
|
|
62
|
+
throw new SnapshotError(`--confirmed ${k}: not a member of ${inputs.id}`);
|
|
63
|
+
}
|
|
64
|
+
const report = {
|
|
65
|
+
baseline: inputs.id,
|
|
66
|
+
verified_at: inputs.today ?? new Date().toISOString().slice(0, 10),
|
|
67
|
+
result: failures.length === 0 ? (w ? "pass-with-waiver" : "pass") : "fail",
|
|
68
|
+
...(w ? { waiver: w } : {}),
|
|
69
|
+
members: rows,
|
|
70
|
+
};
|
|
71
|
+
await writeFile(join(inputs.areaRoot, inputs.id, "provenance.yaml"), stringify(report, { lineWidth: 0 }));
|
|
72
|
+
for (const r of rows)
|
|
73
|
+
write(`${r.canonical_repo_key}: local ${r.local}; canonical ${r.canonical}\n`);
|
|
74
|
+
if (w)
|
|
75
|
+
write(`WAIVER in force — ${w.who}, ${w.date}: ${w.why}\n`);
|
|
76
|
+
if (failures.length > 0) {
|
|
77
|
+
write(`FAIL: ${failures.length} problem(s); written to provenance.yaml\n`);
|
|
78
|
+
return { exitCode: 1 };
|
|
79
|
+
}
|
|
80
|
+
write(`${report.result}: ${rows.length} member(s)\n`);
|
|
81
|
+
return { exitCode: 0 };
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
if (e instanceof SnapshotError) {
|
|
85
|
+
write(`REFUSED: ${e.message}\n`);
|
|
86
|
+
return { exitCode: 1 };
|
|
87
|
+
}
|
|
88
|
+
throw e;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Briefing-layer sync per ADR 0148.
|
|
3
|
+
*
|
|
4
|
+
* Two files participate, with different semantics:
|
|
5
|
+
*
|
|
6
|
+
* - `agent-instructions.md` — fully method-managed; unconditional
|
|
7
|
+
* overwrite on every sync. No marker-aware merge.
|
|
8
|
+
* - The wrappers, `CLAUDE.md` and the `AGENTS.md` pointer (M-ADR 0220 §4.6) — partially
|
|
9
|
+
* method-managed. The `<!-- method-managed: begin/end -->` block
|
|
10
|
+
* is overwritten; adopter content around it survives via the
|
|
11
|
+
* existing `writeMarkedBlockFile` machinery (same pattern as
|
|
12
|
+
* `.github/workflows/method-validation.yml`). Blank regions
|
|
13
|
+
* around the block are scaffold-filled from the template so
|
|
14
|
+
* template-shape changes (heading, adopter-guidance scaffold)
|
|
15
|
+
* propagate instead of drifting — non-blank adopter regions are
|
|
16
|
+
* never touched.
|
|
17
|
+
*
|
|
18
|
+
* Both files are rendered from templates that ship in the bundle at
|
|
19
|
+
* `<target>/orchestration/templates/briefing/...`. Older bundles
|
|
20
|
+
* pre-dating ADR 0148 won't carry the templates; this module skips
|
|
21
|
+
* gracefully with a log line in that case.
|
|
22
|
+
*/
|
|
23
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
24
|
+
import { dirname, join } from "node:path";
|
|
25
|
+
import { AGENT_INSTRUCTIONS_REL_PATH, WRAPPER_BLOCK_BEGIN, WRAPPER_BLOCK_END, briefingWrappers, renderAgentInstructions, renderWrapper, resolveBriefingVariables, splitWrapperTemplate, } from "../briefing/index.js";
|
|
26
|
+
import { writeMarkedBlockFile } from "./yaml-block-merge.js";
|
|
27
|
+
export async function syncBriefingFiles(opts) {
|
|
28
|
+
const log = [];
|
|
29
|
+
const vars = await resolveBriefingVariables({
|
|
30
|
+
targetRepo: opts.targetRepo,
|
|
31
|
+
repoKindOverride: opts.repoKind,
|
|
32
|
+
baselineVersionOverride: opts.baselineVersion,
|
|
33
|
+
});
|
|
34
|
+
// 1. agent-instructions.md — unconditional overwrite, once no wrapper will refuse.
|
|
35
|
+
const aiRender = await renderAgentInstructions(opts.targetRepo, vars);
|
|
36
|
+
if (aiRender === null) {
|
|
37
|
+
log.push(`Skipped ${AGENT_INSTRUCTIONS_REL_PATH}: bundle does not ship a briefing template (pre-ADR-0148 baseline).`);
|
|
38
|
+
// If the agent-instructions template is missing the wrapper ones almost
|
|
39
|
+
// certainly are too — skip the rest as well.
|
|
40
|
+
return { log, refused: null };
|
|
41
|
+
}
|
|
42
|
+
// 2. The wrappers, rendered first so a refusal leaves nothing written. A baseline that does not
|
|
43
|
+
// ship a wrapper's template (the AGENTS.md pointer arrived with M-ADR 0220) skips that wrapper.
|
|
44
|
+
const wrappers = [];
|
|
45
|
+
for (const w of briefingWrappers(opts.agentHarness)) {
|
|
46
|
+
const render = await renderWrapper(opts.targetRepo, w.templateRel, vars);
|
|
47
|
+
if (render === null) {
|
|
48
|
+
log.push(`Skipped ${w.outputRel}: the baseline does not ship ${w.templateRel}.`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (render.unresolved.length > 0) {
|
|
52
|
+
log.push(`Warning: ${w.outputRel} has unresolved placeholders: ${render.unresolved.join(", ")}`);
|
|
53
|
+
}
|
|
54
|
+
wrappers.push({ rel: w.outputRel, content: render.content });
|
|
55
|
+
}
|
|
56
|
+
if (!opts.allowOverwrite) {
|
|
57
|
+
for (const w of wrappers) {
|
|
58
|
+
const existing = await readIfExists(join(opts.targetRepo, w.rel));
|
|
59
|
+
if (existing !== null && existing !== "" && !(existing.includes(WRAPPER_BLOCK_BEGIN) && existing.includes(WRAPPER_BLOCK_END))) {
|
|
60
|
+
return { log, refused: w.rel };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (aiRender.unresolved.length > 0) {
|
|
65
|
+
log.push(`Warning: agent-instructions.md has unresolved placeholders: ${aiRender.unresolved.join(", ")}`);
|
|
66
|
+
}
|
|
67
|
+
const aiPath = join(opts.targetRepo, AGENT_INSTRUCTIONS_REL_PATH);
|
|
68
|
+
await mkdir(dirname(aiPath), { recursive: true });
|
|
69
|
+
await writeFile(aiPath, aiRender.content, "utf8");
|
|
70
|
+
log.push(`Refreshed ${AGENT_INSTRUCTIONS_REL_PATH}.`);
|
|
71
|
+
// 3. Each wrapper — marker-aware merge. Mirror the ci-workflow sync pattern: when the target
|
|
72
|
+
// file does not yet exist, write the full rendered template (block + adopter section template).
|
|
73
|
+
// When it exists, write the method-managed block and scaffold-fill blank regions around it from
|
|
74
|
+
// the template — writeMarkedBlockFile preserves non-blank adopter content verbatim.
|
|
75
|
+
for (const w of wrappers) {
|
|
76
|
+
const wrapperPath = join(opts.targetRepo, w.rel);
|
|
77
|
+
const wrapperExists = await fileExists(wrapperPath);
|
|
78
|
+
const templateParts = splitWrapperTemplate(w.content);
|
|
79
|
+
const mergeResult = await writeMarkedBlockFile(wrapperPath, wrapperExists ? templateParts.block : w.content, { begin: WRAPPER_BLOCK_BEGIN, end: WRAPPER_BLOCK_END }, opts.allowOverwrite, wrapperExists ? { pre: templateParts.pre, post: templateParts.post } : undefined);
|
|
80
|
+
if (mergeResult.refused) {
|
|
81
|
+
return { log, refused: w.rel };
|
|
82
|
+
}
|
|
83
|
+
if (mergeResult.changed) {
|
|
84
|
+
log.push(`Refreshed ${w.rel}.`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { log, refused: null };
|
|
88
|
+
}
|
|
89
|
+
async function readIfExists(path) {
|
|
90
|
+
try {
|
|
91
|
+
return await readFile(path, "utf8");
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async function fileExists(path) {
|
|
98
|
+
try {
|
|
99
|
+
await access(path);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=briefing-merge.js.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { appendFile, copyFile, mkdir, readFile, readdir, rm } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Consumer-side bundle cache (ADR 0176 §4.2).
|
|
5
|
+
*
|
|
6
|
+
* Every consumed non-forward-base bundle is extracted to
|
|
7
|
+
* `<targetRepo>/.sdd/bundle-cache/<bundleName>@<version>/` — the
|
|
8
|
+
* resolution substrate for `sdd-cli scaffold domain --archetype` and
|
|
9
|
+
* the staging source for materialise-mode paths. The cache directory
|
|
10
|
+
* is gitignored (ensured at sync time).
|
|
11
|
+
*
|
|
12
|
+
* Cache layout per bundle: the bundle's payload tree, verbatim (so
|
|
13
|
+
* registry.yaml and scaffold-source files sit at their shipped paths).
|
|
14
|
+
* Stale versions of the same bundle name are pruned on refresh.
|
|
15
|
+
*/
|
|
16
|
+
export const BUNDLE_CACHE_REL = ".sdd/bundle-cache";
|
|
17
|
+
/**
|
|
18
|
+
* The registry file name (ADR 0176 §4.1) — always cache-only. A bundle may
|
|
19
|
+
* ship its registry at the payload root or nested (a multi-bundle producer
|
|
20
|
+
* repo keeps one registry per bundle subtree, e.g. a vertical overlay's
|
|
21
|
+
* `verticals/fleet/registry.yaml`, because the producer-layout rule forbids
|
|
22
|
+
* path mapping and the root name can only be claimed once).
|
|
23
|
+
*/
|
|
24
|
+
export const REGISTRY_FILE = "registry.yaml";
|
|
25
|
+
/**
|
|
26
|
+
* The application-archetype vocabulary file name (M-ADR 0191 §4.2) — always
|
|
27
|
+
* cache-only, like the registry. Carries the seedless classification
|
|
28
|
+
* vocabulary a bundle contributes (resolution data, not adopter-editable
|
|
29
|
+
* content). May sit at the payload root (core layer) or nested (a vertical
|
|
30
|
+
* overlay's `verticals/<v>/application-archetypes.yaml`), same as the
|
|
31
|
+
* registry.
|
|
32
|
+
*/
|
|
33
|
+
export const VOCAB_FILE = "application-archetypes.yaml";
|
|
34
|
+
/**
|
|
35
|
+
* Split a bundle's managed paths into the set to materialise into the
|
|
36
|
+
* repo tree vs the set that stays cache-only (ADR 0176 §4.3):
|
|
37
|
+
* scaffold-source-prefixed paths, registry files, and the archetype
|
|
38
|
+
* vocabulary file are cache-only.
|
|
39
|
+
*/
|
|
40
|
+
export function splitConsumptionModes(managedPaths, scaffoldSourcePaths) {
|
|
41
|
+
const prefixes = scaffoldSourcePaths ?? [];
|
|
42
|
+
const materialise = [];
|
|
43
|
+
const cacheOnly = [];
|
|
44
|
+
for (const p of managedPaths) {
|
|
45
|
+
const isScaffoldSource = prefixes.some((prefix) => p.startsWith(prefix));
|
|
46
|
+
const isRegistry = p === REGISTRY_FILE || p.endsWith(`/${REGISTRY_FILE}`);
|
|
47
|
+
const isVocab = p === VOCAB_FILE || p.endsWith(`/${VOCAB_FILE}`);
|
|
48
|
+
if (isScaffoldSource || isRegistry || isVocab) {
|
|
49
|
+
cacheOnly.push(p);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
materialise.push(p);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { materialise, cacheOnly };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Extract a bundle's payload into the cache, replacing any previously
|
|
59
|
+
* cached version of the same bundle name (one cached version per
|
|
60
|
+
* bundle — the lock pins exactly one).
|
|
61
|
+
*/
|
|
62
|
+
export async function cacheBundle(input) {
|
|
63
|
+
const cacheRoot = join(input.targetRepo, BUNDLE_CACHE_REL);
|
|
64
|
+
await prunePreviousVersions(cacheRoot, input.bundleName);
|
|
65
|
+
const dest = join(cacheRoot, `${input.bundleName}@${input.bundleVersion}`);
|
|
66
|
+
for (const p of input.managedPaths) {
|
|
67
|
+
const src = join(input.payloadDir, p);
|
|
68
|
+
const dst = join(dest, p);
|
|
69
|
+
await mkdir(dirname(dst), { recursive: true });
|
|
70
|
+
await copyFile(src, dst);
|
|
71
|
+
}
|
|
72
|
+
return dest;
|
|
73
|
+
}
|
|
74
|
+
async function prunePreviousVersions(cacheRoot, bundleName) {
|
|
75
|
+
let entries;
|
|
76
|
+
try {
|
|
77
|
+
entries = await readdir(cacheRoot, { withFileTypes: true });
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return; // no cache yet
|
|
81
|
+
}
|
|
82
|
+
for (const e of entries) {
|
|
83
|
+
if (e.isDirectory() && e.name.startsWith(`${bundleName}@`)) {
|
|
84
|
+
await rm(join(cacheRoot, e.name), { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Ensure the consumer's .gitignore excludes the bundle cache. Appends
|
|
90
|
+
* a marked line when absent; leaves adopter content untouched otherwise.
|
|
91
|
+
*/
|
|
92
|
+
export async function ensureCacheGitignored(targetRepo) {
|
|
93
|
+
const giPath = join(targetRepo, ".gitignore");
|
|
94
|
+
const line = `${BUNDLE_CACHE_REL}/`;
|
|
95
|
+
let existing = "";
|
|
96
|
+
try {
|
|
97
|
+
existing = await readFile(giPath, "utf8");
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// no .gitignore yet — create one below
|
|
101
|
+
}
|
|
102
|
+
if (existing.split("\n").some((l) => l.trim() === line || l.trim() === BUNDLE_CACHE_REL)) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
const prefixNewline = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
106
|
+
await appendFile(giPath, `${prefixNewline}# sdd-cli bundle cache (ADR 0176) — extracted bundle payloads, not source\n${line}\n`);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=bundle-cache.js.map
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge-aware write for files with `# method-managed: begin/end`
|
|
3
|
+
* markers. The method block is replaced; content outside the block
|
|
4
|
+
* (including content above a lone trailing block, or below it, or
|
|
5
|
+
* both) is preserved verbatim.
|
|
6
|
+
*
|
|
7
|
+
* Conflict model (per ADR 0114, narrowed to block scope):
|
|
8
|
+
* - If the existing file has no markers, treat it as adopter-authored.
|
|
9
|
+
* Refuse to replace unless `allowOverwrite` is true.
|
|
10
|
+
* - If the existing file has markers, upsert the block. Lines outside
|
|
11
|
+
* the markers survive unchanged.
|
|
12
|
+
* - Drift inside the block does not block here — callers compare the
|
|
13
|
+
* in-block slice to the incoming slice separately if they need
|
|
14
|
+
* refuse-on-drift semantics (see `detectBlockDrift`).
|
|
15
|
+
*/
|
|
16
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
17
|
+
import { dirname } from "node:path";
|
|
18
|
+
export function mergeMarkedBlock(opts) {
|
|
19
|
+
const existing = opts.existing ?? "";
|
|
20
|
+
const { begin, end } = opts.markers;
|
|
21
|
+
const beginIdx = existing.indexOf(begin);
|
|
22
|
+
const endIdx = existing.indexOf(end, beginIdx === -1 ? 0 : beginIdx);
|
|
23
|
+
if (existing === "") {
|
|
24
|
+
return {
|
|
25
|
+
output: opts.incomingBlock.endsWith("\n")
|
|
26
|
+
? opts.incomingBlock
|
|
27
|
+
: opts.incomingBlock + "\n",
|
|
28
|
+
refused: false,
|
|
29
|
+
changed: true,
|
|
30
|
+
preAdopter: "",
|
|
31
|
+
postAdopter: "",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (beginIdx === -1 || endIdx === -1) {
|
|
35
|
+
// Existing file has no markers — treat as adopter-authored.
|
|
36
|
+
if (!opts.allowOverwrite) {
|
|
37
|
+
return {
|
|
38
|
+
output: existing,
|
|
39
|
+
refused: true,
|
|
40
|
+
changed: false,
|
|
41
|
+
preAdopter: existing,
|
|
42
|
+
postAdopter: "",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// Overwrite: replace the whole file with the method block.
|
|
46
|
+
return {
|
|
47
|
+
output: opts.incomingBlock.endsWith("\n")
|
|
48
|
+
? opts.incomingBlock
|
|
49
|
+
: opts.incomingBlock + "\n",
|
|
50
|
+
refused: false,
|
|
51
|
+
changed: existing !== opts.incomingBlock,
|
|
52
|
+
preAdopter: existing,
|
|
53
|
+
postAdopter: "",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
let preAdopter = existing.slice(0, beginIdx);
|
|
57
|
+
let postAdopter = existing.slice(endIdx + end.length);
|
|
58
|
+
if (opts.scaffold?.pre !== undefined && preAdopter.trim() === "") {
|
|
59
|
+
preAdopter = opts.scaffold.pre;
|
|
60
|
+
}
|
|
61
|
+
if (opts.scaffold?.post !== undefined && postAdopter.trim() === "") {
|
|
62
|
+
postAdopter = opts.scaffold.post;
|
|
63
|
+
}
|
|
64
|
+
const output = preAdopter +
|
|
65
|
+
opts.incomingBlock +
|
|
66
|
+
(postAdopter.startsWith("\n") ? postAdopter : "\n" + postAdopter);
|
|
67
|
+
const finalOutput = output.endsWith("\n") ? output : output + "\n";
|
|
68
|
+
return {
|
|
69
|
+
output: finalOutput,
|
|
70
|
+
refused: false,
|
|
71
|
+
changed: finalOutput !== existing,
|
|
72
|
+
preAdopter,
|
|
73
|
+
postAdopter,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Detect whether the method block in `existing` differs from the
|
|
78
|
+
* incoming block. Returns true if the adopter has edited inside the
|
|
79
|
+
* markers (drift); false if no markers, exact match, or no file.
|
|
80
|
+
* Callers use this to implement refuse-on-drift policy where desired.
|
|
81
|
+
*/
|
|
82
|
+
export function detectBlockDrift(existing, incomingBlock, markers) {
|
|
83
|
+
if (!existing)
|
|
84
|
+
return false;
|
|
85
|
+
const beginIdx = existing.indexOf(markers.begin);
|
|
86
|
+
const endIdx = existing.indexOf(markers.end, beginIdx === -1 ? 0 : beginIdx);
|
|
87
|
+
if (beginIdx === -1 || endIdx === -1)
|
|
88
|
+
return false;
|
|
89
|
+
const currentBlock = existing.slice(beginIdx, endIdx + markers.end.length);
|
|
90
|
+
return currentBlock !== incomingBlock.trimEnd();
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Apply the merge and write the result to disk. Creates the target
|
|
94
|
+
* directory if absent.
|
|
95
|
+
*/
|
|
96
|
+
export async function writeMarkedBlockFile(absPath, incomingBlock, markers, allowOverwrite, scaffold) {
|
|
97
|
+
const existing = await tryReadFile(absPath);
|
|
98
|
+
const result = mergeMarkedBlock({
|
|
99
|
+
existing,
|
|
100
|
+
incomingBlock,
|
|
101
|
+
markers,
|
|
102
|
+
allowOverwrite,
|
|
103
|
+
scaffold,
|
|
104
|
+
});
|
|
105
|
+
if (!result.refused && result.changed) {
|
|
106
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
107
|
+
await writeFile(absPath, result.output, "utf8");
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
async function tryReadFile(path) {
|
|
112
|
+
try {
|
|
113
|
+
await access(path);
|
|
114
|
+
return await readFile(path, "utf8");
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=yaml-block-merge.js.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const WARN_MARKER = "[WARN]";
|
|
2
|
+
// A validator that ran over an EMPTY population says so with an `[N/A]` line
|
|
3
|
+
// and prints no `[PASS]`. Counting it as passed is the vacuous pass the
|
|
4
|
+
// empty-set sweep measured (an empty platform SDD validated `passed=8`); it is
|
|
5
|
+
// counted apart instead (R1, ruled 2026-09-29). Exit codes are unchanged: a
|
|
6
|
+
// run that measured nothing neither passes nor fails the gate — it is named.
|
|
7
|
+
// A script that measured PART of its population prints real `[PASS]` lines,
|
|
8
|
+
// so a stray `[N/A]` on one sub-check never demotes a real pass.
|
|
9
|
+
const NA_LINE = /^\[N\/A\]/m;
|
|
10
|
+
const PASS_LINE = /^\[PASS\]/m;
|
|
11
|
+
export function classifyResult(r, opts) {
|
|
12
|
+
if (r.status === "skip")
|
|
13
|
+
return "skipped";
|
|
14
|
+
if (r.exitCode !== 0)
|
|
15
|
+
return "failed";
|
|
16
|
+
const hasWarn = r.stdout.includes(WARN_MARKER);
|
|
17
|
+
if (opts.strict && hasWarn)
|
|
18
|
+
return "failed";
|
|
19
|
+
if (NA_LINE.test(r.stdout) && !PASS_LINE.test(r.stdout))
|
|
20
|
+
return "not-measured";
|
|
21
|
+
return hasWarn ? "warned" : "passed";
|
|
22
|
+
}
|
|
23
|
+
export function aggregateResults(results, opts) {
|
|
24
|
+
let passed = 0;
|
|
25
|
+
let failed = 0;
|
|
26
|
+
let skipped = 0;
|
|
27
|
+
let warned = 0;
|
|
28
|
+
let notMeasured = 0;
|
|
29
|
+
for (const r of results) {
|
|
30
|
+
const outcome = classifyResult(r, opts);
|
|
31
|
+
if (r.status === "run" && r.exitCode === 0 && r.stdout.includes(WARN_MARKER))
|
|
32
|
+
warned++;
|
|
33
|
+
if (outcome === "skipped")
|
|
34
|
+
skipped++;
|
|
35
|
+
else if (outcome === "failed")
|
|
36
|
+
failed++;
|
|
37
|
+
else if (outcome === "not-measured")
|
|
38
|
+
notMeasured++;
|
|
39
|
+
else
|
|
40
|
+
passed++;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
exitCode: failed > 0 ? 1 : 0,
|
|
44
|
+
passed,
|
|
45
|
+
failed,
|
|
46
|
+
skipped,
|
|
47
|
+
warned,
|
|
48
|
+
notMeasured,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=aggregate.js.map
|