@sdd-method/maintain-cli 0.0.0-stage → 0.1.0-rc.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/lib/adr-origin.js +59 -0
- package/dist/lib/agent-harness.js +28 -0
- package/dist/lib/briefing/index.js +13 -0
- package/dist/lib/briefing/render.js +123 -0
- package/dist/lib/briefing/variables.js +92 -0
- package/dist/lib/bundle.js +123 -0
- package/dist/lib/catalogue/aggregate.js +2404 -0
- package/dist/lib/catalogue/aggregation-manifest-loader.js +173 -0
- package/dist/lib/catalogue/application-archetype/expectations.js +119 -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/contract-path-resolution.js +149 -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-item-coverage.js +142 -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 +69 -0
- package/dist/lib/catalogue/gate-checks.js +338 -0
- package/dist/lib/catalogue/graph-projection.js +346 -0
- package/dist/lib/catalogue/graph-run.js +64 -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/ci-binding/index.js +209 -0
- package/dist/lib/ci-binding/module.js +204 -0
- package/dist/lib/conformance/binding.js +70 -0
- package/dist/lib/context-pull/index.js +151 -0
- package/dist/lib/context-pull/resolve.js +70 -0
- package/dist/lib/generate-briefing/index.js +77 -0
- package/dist/lib/hooks-seed/copilot.js +167 -0
- package/dist/lib/hooks-seed/entries.js +47 -0
- package/dist/lib/hooks-seed/index.js +17 -0
- package/dist/lib/hooks-seed/lifecycle-mapping.js +28 -0
- package/dist/lib/hooks-seed/merge.js +128 -0
- package/dist/lib/hooks-seed/types.js +20 -0
- package/dist/lib/init/ci-workflow.js +89 -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 +432 -0
- package/dist/lib/mcp-seed/entries.js +14 -0
- package/dist/lib/mcp-seed/index.js +28 -0
- package/dist/lib/mcp-seed/merge.js +99 -0
- package/dist/lib/mcp-seed/types.js +13 -0
- package/dist/lib/pack/builder.js +303 -0
- package/dist/lib/pack/index.js +87 -0
- package/dist/lib/pack/leakage.js +144 -0
- package/dist/lib/pack/log.js +18 -0
- package/dist/lib/pack/publish-transform.js +233 -0
- package/dist/lib/pack/reference-closure.js +125 -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/repo-kind.js +129 -0
- package/dist/lib/scripts.js +7 -0
- package/dist/lib/snapshot/catalogue-diff.js +219 -0
- package/dist/lib/snapshot/manifest.js +206 -0
- package/dist/lib/sync/adr-transitions.js +250 -0
- package/dist/lib/sync/apply.js +91 -0
- package/dist/lib/sync/briefing-merge.js +90 -0
- package/dist/lib/sync/bundle-cache.js +109 -0
- package/dist/lib/sync/bundle-source.js +61 -0
- package/dist/lib/sync/config.js +72 -0
- package/dist/lib/sync/copilot-hooks-merge.js +108 -0
- package/dist/lib/sync/diff.js +251 -0
- package/dist/lib/sync/index.js +809 -0
- package/dist/lib/sync/settings-merge.js +216 -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-bin.js +8 -0
- package/dist/maintain-program.js +91 -0
- package/dist/maintain-version.json +4 -0
- package/dist/verbs/catalogue-maintain.js +110 -0
- package/dist/verbs/catalogue-verify.js +114 -0
- package/dist/verbs/context.js +39 -0
- package/dist/verbs/generate-briefing.js +29 -0
- package/dist/verbs/list.js +45 -0
- package/dist/verbs/pack-build.js +31 -0
- package/dist/verbs/pack-validate.js +17 -0
- package/dist/verbs/path-option.js +25 -0
- package/dist/verbs/render.js +87 -0
- package/dist/verbs/sync.js +98 -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,206 @@
|
|
|
1
|
+
// Implementation-set snapshot manifests (`<area>/<baseline-id>/snapshot.yaml`)
|
|
2
|
+
// and the file-level primitives the `snapshot` verbs share.
|
|
3
|
+
//
|
|
4
|
+
// The shape is the method's, not this module's: reverse/artifact-conventions.md
|
|
5
|
+
// "Implementation-Set Snapshots". Where the conventions REQUIRE a companion
|
|
6
|
+
// field (`carried_reason` beside `carried_from`, `reverification` beside
|
|
7
|
+
// `reverified_at`), readManifest refuses a manifest without it — a carried member
|
|
8
|
+
// with no reason cannot be told apart from an unrecorded removal.
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
11
|
+
import { join, relative, sep } from "node:path";
|
|
12
|
+
import { parse } from "yaml";
|
|
13
|
+
/** The line every later verb prints while a baseline carries unresolved credential findings. */
|
|
14
|
+
export function credentialNotice(m) {
|
|
15
|
+
const s = m.credential_scan;
|
|
16
|
+
if (!s || s.result === "clean")
|
|
17
|
+
return null;
|
|
18
|
+
return `CREDENTIAL FINDINGS on record for ${m.snapshot_id}: ${s.result} — ${s.disposition ?? "NO DISPOSITION"}`;
|
|
19
|
+
}
|
|
20
|
+
export class SnapshotError extends Error {
|
|
21
|
+
}
|
|
22
|
+
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
23
|
+
export function isIsoDate(s) {
|
|
24
|
+
if (typeof s !== "string" || !DATE.test(s))
|
|
25
|
+
return false;
|
|
26
|
+
const d = new Date(`${s}T00:00:00Z`);
|
|
27
|
+
return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === s;
|
|
28
|
+
}
|
|
29
|
+
/** `name-branch@rev` → { stem: "name-branch", revision: "rev" }. */
|
|
30
|
+
export function splitFolder(folder) {
|
|
31
|
+
const at = folder.lastIndexOf("@");
|
|
32
|
+
if (at <= 0 || at === folder.length - 1)
|
|
33
|
+
return null;
|
|
34
|
+
return { stem: folder.slice(0, at), revision: folder.slice(at + 1) };
|
|
35
|
+
}
|
|
36
|
+
/** Member folders (`*@*` directories) directly under a baseline directory. */
|
|
37
|
+
export async function memberFolders(baselineDir) {
|
|
38
|
+
const out = [];
|
|
39
|
+
for (const e of await readdir(baselineDir, { withFileTypes: true })) {
|
|
40
|
+
if (e.isDirectory() && splitFolder(e.name) !== null)
|
|
41
|
+
out.push(e.name);
|
|
42
|
+
}
|
|
43
|
+
return out.sort();
|
|
44
|
+
}
|
|
45
|
+
/** Dated baseline ids under an area, ascending. */
|
|
46
|
+
export async function baselineIds(areaRoot) {
|
|
47
|
+
const out = [];
|
|
48
|
+
for (const e of await readdir(areaRoot, { withFileTypes: true })) {
|
|
49
|
+
if (e.isDirectory() && isIsoDate(e.name))
|
|
50
|
+
out.push(e.name);
|
|
51
|
+
}
|
|
52
|
+
return out.sort();
|
|
53
|
+
}
|
|
54
|
+
export async function exists(p) {
|
|
55
|
+
try {
|
|
56
|
+
await stat(p);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function manifestPath(areaRoot, id) {
|
|
64
|
+
return join(areaRoot, id, "snapshot.yaml");
|
|
65
|
+
}
|
|
66
|
+
export async function readManifest(areaRoot, id) {
|
|
67
|
+
const p = manifestPath(areaRoot, id);
|
|
68
|
+
if (!(await exists(p)))
|
|
69
|
+
throw new SnapshotError(`no manifest at ${p}`);
|
|
70
|
+
const raw = parse(await readFile(p, "utf8"));
|
|
71
|
+
if (raw === null || typeof raw !== "object")
|
|
72
|
+
throw new SnapshotError(`${p}: not a mapping`);
|
|
73
|
+
const problems = [];
|
|
74
|
+
if (raw.snapshot_id !== id)
|
|
75
|
+
problems.push(`snapshot_id '${String(raw.snapshot_id)}' does not match its folder '${id}'`);
|
|
76
|
+
if (!isIsoDate(raw.observed_at))
|
|
77
|
+
problems.push("observed_at must be YYYY-MM-DD");
|
|
78
|
+
if (typeof raw.implementation !== "string" || raw.implementation === "")
|
|
79
|
+
problems.push("implementation missing");
|
|
80
|
+
if (raw.prior_snapshot !== null && raw.prior_snapshot !== undefined && !isIsoDate(raw.prior_snapshot)) {
|
|
81
|
+
problems.push("prior_snapshot must be a baseline id or null");
|
|
82
|
+
}
|
|
83
|
+
if (!Array.isArray(raw.members))
|
|
84
|
+
problems.push("members must be a list");
|
|
85
|
+
// A hand-written revision of digits only (`50834756022`) parses as a NUMBER, and
|
|
86
|
+
// a strict comparison with the folder's string suffix then calls an unchanged
|
|
87
|
+
// member changed. Identity fields are strings, whatever YAML made of them.
|
|
88
|
+
for (const m of raw.members ?? []) {
|
|
89
|
+
if (m && typeof m === "object") {
|
|
90
|
+
for (const f of ["canonical_repo_key", "exported_folder_name", "source_revision"]) {
|
|
91
|
+
if (typeof m[f] === "number")
|
|
92
|
+
m[f] = String(m[f]);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (const r of raw.members_removed ?? []) {
|
|
97
|
+
if (r && typeof r.last_revision === "number")
|
|
98
|
+
r.last_revision = String(r.last_revision);
|
|
99
|
+
}
|
|
100
|
+
for (const [i, m] of (raw.members ?? []).entries()) {
|
|
101
|
+
const at = `members[${i}] (${m?.canonical_repo_key ?? "?"})`;
|
|
102
|
+
if (!m?.canonical_repo_key || !m.exported_folder_name || !m.source_revision) {
|
|
103
|
+
problems.push(`${at}: canonical_repo_key, exported_folder_name and source_revision are required`);
|
|
104
|
+
}
|
|
105
|
+
if (m?.carried_from !== undefined && !m.carried_reason)
|
|
106
|
+
problems.push(`${at}: carried_from requires carried_reason`);
|
|
107
|
+
if (m?.reverified_at !== undefined && !m.reverification)
|
|
108
|
+
problems.push(`${at}: reverified_at requires reverification`);
|
|
109
|
+
}
|
|
110
|
+
for (const [i, r] of (raw.members_removed ?? []).entries()) {
|
|
111
|
+
if (!r?.canonical_repo_key || !r.last_revision || !r.reason) {
|
|
112
|
+
problems.push(`members_removed[${i}]: canonical_repo_key, last_revision and reason are required`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (problems.length > 0)
|
|
116
|
+
throw new SnapshotError(`${p}:\n - ${problems.join("\n - ")}`);
|
|
117
|
+
return raw;
|
|
118
|
+
}
|
|
119
|
+
/** Where a member's bytes live: its own baseline, or the one it was carried from. */
|
|
120
|
+
export function memberBytesDir(areaRoot, manifest, m) {
|
|
121
|
+
return join(areaRoot, m.carried_from ?? manifest.snapshot_id, m.exported_folder_name);
|
|
122
|
+
}
|
|
123
|
+
/** relative path → sha256 for every file under a directory. */
|
|
124
|
+
export async function hashTree(root) {
|
|
125
|
+
const out = new Map();
|
|
126
|
+
async function walk(dir) {
|
|
127
|
+
for (const e of await readdir(dir, { withFileTypes: true })) {
|
|
128
|
+
const full = join(dir, e.name);
|
|
129
|
+
if (e.isDirectory()) {
|
|
130
|
+
if (e.name === ".git")
|
|
131
|
+
continue;
|
|
132
|
+
await walk(full);
|
|
133
|
+
}
|
|
134
|
+
else if (e.isFile()) {
|
|
135
|
+
const h = createHash("sha256").update(await readFile(full)).digest("hex");
|
|
136
|
+
out.set(relative(root, full).split(sep).join("/"), h);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
await walk(root);
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
export function diffTrees(before, after) {
|
|
144
|
+
const added = [];
|
|
145
|
+
const removed = [];
|
|
146
|
+
const modified = [];
|
|
147
|
+
for (const [p, h] of after) {
|
|
148
|
+
const b = before.get(p);
|
|
149
|
+
if (b === undefined)
|
|
150
|
+
added.push(p);
|
|
151
|
+
else if (b !== h)
|
|
152
|
+
modified.push(p);
|
|
153
|
+
}
|
|
154
|
+
for (const p of before.keys())
|
|
155
|
+
if (!after.has(p))
|
|
156
|
+
removed.push(p);
|
|
157
|
+
return { added: added.sort(), removed: removed.sort(), modified: modified.sort() };
|
|
158
|
+
}
|
|
159
|
+
const BRANCH_SUFFIX = /-(master|main|develop|trunk)$/;
|
|
160
|
+
/**
|
|
161
|
+
* The repository ids an SDD declares: every `orchestration/repo-config/**\/<id>.yaml` file
|
|
162
|
+
* name and every id in its built `catalogues/repositories.csv`. Empty when neither exists.
|
|
163
|
+
*/
|
|
164
|
+
export async function knownRepositoryIds(sddPath) {
|
|
165
|
+
const out = new Set();
|
|
166
|
+
const walk = async (dir) => {
|
|
167
|
+
if (!(await exists(dir)))
|
|
168
|
+
return;
|
|
169
|
+
for (const e of await readdir(dir, { withFileTypes: true })) {
|
|
170
|
+
if (e.isDirectory())
|
|
171
|
+
await walk(join(dir, e.name));
|
|
172
|
+
else if (/\.ya?ml$/.test(e.name))
|
|
173
|
+
out.add(e.name.replace(/\.ya?ml$/, ""));
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
await walk(join(sddPath, "orchestration", "repo-config"));
|
|
177
|
+
const csv = join(sddPath, "catalogues", "repositories.csv");
|
|
178
|
+
if (await exists(csv)) {
|
|
179
|
+
for (const line of (await readFile(csv, "utf8")).split("\n").slice(1)) {
|
|
180
|
+
const id = line.split(",")[0]?.trim();
|
|
181
|
+
if (id)
|
|
182
|
+
out.add(id);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* A canonical_repo_key derived from a folder's stem, checked. Stripping a branch
|
|
189
|
+
* suffix only works for the four names it knows: a `-_dev` or `-release-1.X` branch
|
|
190
|
+
* leaves a key that is no repository id, and nothing downstream says so until a
|
|
191
|
+
* catalogue build cannot resolve it. With the SDD's ids known, such a key is a
|
|
192
|
+
* PROBLEM naming the nearest id; without them it is marked unverified.
|
|
193
|
+
*/
|
|
194
|
+
export function deriveKey(folder, stem, known) {
|
|
195
|
+
const key = stem.replace(BRANCH_SUFFIX, "");
|
|
196
|
+
if (known.size === 0)
|
|
197
|
+
return { key, unverified: true };
|
|
198
|
+
if (known.has(key))
|
|
199
|
+
return { key };
|
|
200
|
+
const nearest = [...known].filter((id) => stem.startsWith(`${id}-`)).sort((a, b) => b.length - a.length)[0];
|
|
201
|
+
return {
|
|
202
|
+
key,
|
|
203
|
+
problem: `${folder}: derived canonical_repo_key '${key}' is not a repository id of this SDD — ${nearest ? `nearest is '${nearest}' (a branch suffix?): pass --key ${folder}=${nearest}` : `pass --key ${folder}=<repository id>`}`,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// ADR transitions (R6 item 12) — the bundle-declared rename / withdrawal /
|
|
2
|
+
// reassignment log, applied at sync.
|
|
3
|
+
//
|
|
4
|
+
// Every method-baseline bundle embeds `ADR_TRANSITIONS.yaml`. Its stated purpose:
|
|
5
|
+
// "Adopters apply transitions during sync to keep their inherited ADR set aligned
|
|
6
|
+
// with upstream without manual `git rm` / `git mv` cleanup." 95 transitions ship in
|
|
7
|
+
// each baseline today, so this is live, not theoretical.
|
|
8
|
+
//
|
|
9
|
+
// The bash reference implemented it; sdd-cli did not, and the gap was invisible
|
|
10
|
+
// because the parity oracle it was compared against also predated the feature.
|
|
11
|
+
//
|
|
12
|
+
// WHY REMOVAL-BY-OMISSION IS NOT AN ADEQUATE SUBSTITUTE. Sync already deletes any
|
|
13
|
+
// previously-managed path missing from the incoming bundle, which incidentally makes
|
|
14
|
+
// a rename look right (old path deleted, new path copied). But the three kinds have
|
|
15
|
+
// semantics that differ from delete-and-add in ways that matter:
|
|
16
|
+
//
|
|
17
|
+
// - rename: the file should MOVE, so an adopter's local edits travel with it
|
|
18
|
+
// and git sees a rename. Delete-plus-copy discards local edits and
|
|
19
|
+
// loses the history link. Under `--no-delete` it leaves BOTH paths.
|
|
20
|
+
// - withdrawal: the schema is explicit — "do NOT auto-delete the file (the
|
|
21
|
+
// adopter may still depend on the decision). Show the
|
|
22
|
+
// `adopter_action` text. Sync continues." Removal-by-omission does
|
|
23
|
+
// the opposite: it deletes silently. So withdrawn paths must be
|
|
24
|
+
// PROTECTED from removal, which is the one place this module has to
|
|
25
|
+
// reach into the rest of sync.
|
|
26
|
+
// - reassignment: an ADR number now means a different decision. There is no file
|
|
27
|
+
// operation at all — the only correct behaviour is to say so.
|
|
28
|
+
//
|
|
29
|
+
// Version-gated on the bundle_version previously applied FOR THIS BUNDLE, so a
|
|
30
|
+
// re-sync does not re-announce transitions the adopter has already taken.
|
|
31
|
+
import { access, mkdir, rename } from "node:fs/promises";
|
|
32
|
+
import { dirname, join, posix } from "node:path";
|
|
33
|
+
import { readFile } from "node:fs/promises";
|
|
34
|
+
import { parse as parseYaml } from "yaml";
|
|
35
|
+
const EMPTY = { renames: [], warnings: [], protectedFromRemoval: [], movedPaths: [] };
|
|
36
|
+
/**
|
|
37
|
+
* Compare a semver-ish pair, tolerating a leading `v`.
|
|
38
|
+
*
|
|
39
|
+
* The lock records `bundle_version` as `v1.2.0` while `introduced_in_version` is
|
|
40
|
+
* written bare (`1.3.0`). The bash implementation carries the same normalisation
|
|
41
|
+
* with a comment noting that without it "every rename transition is silently
|
|
42
|
+
* skipped" — worth keeping, because the failure is silent in both languages.
|
|
43
|
+
*/
|
|
44
|
+
export function versionLessThan(a, b) {
|
|
45
|
+
const parts = (v) => v.replace(/^v/, "").split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
46
|
+
const [x, y] = [parts(a), parts(b)];
|
|
47
|
+
for (let i = 0; i < Math.max(x.length, y.length); i++) {
|
|
48
|
+
const d = (x[i] ?? 0) - (y[i] ?? 0);
|
|
49
|
+
if (d !== 0)
|
|
50
|
+
return d < 0;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
async function exists(p) {
|
|
55
|
+
try {
|
|
56
|
+
await access(p);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Build the transition plan for this sync.
|
|
65
|
+
*
|
|
66
|
+
* @param bundleRootDir extracted bundle root (holds ADR_TRANSITIONS.yaml)
|
|
67
|
+
* @param targetRepo adopter repo root
|
|
68
|
+
* @param previousVersion bundle_version last applied for THIS bundle, or undefined
|
|
69
|
+
* on a first sync — in which case every transition is in
|
|
70
|
+
* scope, because the adopter has taken none of them.
|
|
71
|
+
*/
|
|
72
|
+
export async function planAdrTransitions(bundleRootDir, targetRepo, previousVersion) {
|
|
73
|
+
const file = join(bundleRootDir, "ADR_TRANSITIONS.yaml");
|
|
74
|
+
if (!(await exists(file)))
|
|
75
|
+
return EMPTY;
|
|
76
|
+
let raw;
|
|
77
|
+
try {
|
|
78
|
+
raw = parseYaml(await readFile(file, "utf8"));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// A malformed transitions file must not block a sync: the payload is still
|
|
82
|
+
// valid and the adopter's tree is still correct without the cleanup.
|
|
83
|
+
return { renames: [], warnings: ["ADR_TRANSITIONS.yaml could not be parsed — transitions skipped"], protectedFromRemoval: [], movedPaths: [] };
|
|
84
|
+
}
|
|
85
|
+
const list = raw?.transitions;
|
|
86
|
+
if (!Array.isArray(list))
|
|
87
|
+
return EMPTY;
|
|
88
|
+
const renames = [];
|
|
89
|
+
const rawWarnings = [];
|
|
90
|
+
const protectedFromRemoval = [];
|
|
91
|
+
const movedPaths = [];
|
|
92
|
+
/**
|
|
93
|
+
* FIRST ADOPTION replays the bundle's whole transition history (ledger §3ae).
|
|
94
|
+
* A repo adopting a bundle for the first time receives its CURRENT state, so
|
|
95
|
+
* every transition in the log happened before this repo existed as an
|
|
96
|
+
* adopter — yet the log is filtered only by "newer than the last applied
|
|
97
|
+
* version", and there is no last applied version, so nothing is filtered.
|
|
98
|
+
* One estate met an ancient reassignment as a fresh warning carrying a
|
|
99
|
+
* colon-form path that read like rename residue to post-wave eyes.
|
|
100
|
+
*
|
|
101
|
+
* Labelled rather than suppressed, deliberately: the notices are TRUE, and a
|
|
102
|
+
* first adopter inheriting a decision that moved slots may still want to know.
|
|
103
|
+
* What was wrong was their apparent currency, not their content — so the fix
|
|
104
|
+
* is to say when they happened, not to hide them.
|
|
105
|
+
*/
|
|
106
|
+
const firstAdoption = previousVersion === undefined || previousVersion === "";
|
|
107
|
+
for (const entry of list) {
|
|
108
|
+
if (typeof entry !== "object" || entry === null)
|
|
109
|
+
continue;
|
|
110
|
+
const kind = String(entry.kind ?? "");
|
|
111
|
+
const adrId = String(entry.adr_id ?? "");
|
|
112
|
+
const introduced = String(entry.introduced_in_version ?? "");
|
|
113
|
+
// Only what is newer than the last applied version. On a first sync
|
|
114
|
+
// (previousVersion undefined) nothing is filtered out.
|
|
115
|
+
if (previousVersion && introduced && !versionLessThan(previousVersion, introduced)) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
switch (kind) {
|
|
119
|
+
case "move":
|
|
120
|
+
case "rename": {
|
|
121
|
+
const fromPath = String(entry.from_path ?? "");
|
|
122
|
+
const toPath = String(entry.to_path ?? "");
|
|
123
|
+
if (!fromPath || !toPath)
|
|
124
|
+
continue;
|
|
125
|
+
const label = kind === "move" ? "move" : `ADR ${adrId} rename`;
|
|
126
|
+
// A rename to itself is a no-op and MUST be silent. Not hypothetical: 92 of
|
|
127
|
+
// the 93 rename entries shipping today have from_path === to_path, because
|
|
128
|
+
// the generator derives from_path from the canon name while the bundle's
|
|
129
|
+
// publish transform has already renamed the file — so by the time the log is
|
|
130
|
+
// embedded, the two have collapsed. Without this guard every one of those
|
|
131
|
+
// would find "both paths exist" (they are the same file) and emit a
|
|
132
|
+
// manual-review warning, burying the real notices in ~92 lines of noise.
|
|
133
|
+
if (fromPath === toPath)
|
|
134
|
+
continue;
|
|
135
|
+
movedPaths.push({ fromPath, toPath, label });
|
|
136
|
+
const fromExists = await exists(join(targetRepo, fromPath));
|
|
137
|
+
const toExists = await exists(join(targetRepo, toPath));
|
|
138
|
+
if (fromExists && toExists) {
|
|
139
|
+
// Both present: the adopter may have copied rather than moved, or has
|
|
140
|
+
// content at both names. Renaming over the top could destroy work.
|
|
141
|
+
rawWarnings.push(`[${label}] both paths exist in adopter repo; manual review needed: ${fromPath} → ${toPath}`);
|
|
142
|
+
}
|
|
143
|
+
else if (fromExists) {
|
|
144
|
+
renames.push({ adrId, fromPath, toPath });
|
|
145
|
+
}
|
|
146
|
+
// Only `toPath` present → already migrated, nothing to do.
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case "withdrawal": {
|
|
150
|
+
const withdrawnPath = String(entry.withdrawn_path ?? "");
|
|
151
|
+
if (!withdrawnPath)
|
|
152
|
+
continue;
|
|
153
|
+
// Protected regardless of presence: it must not be removed by omission on
|
|
154
|
+
// this sync even if a concurrent step would otherwise have swept it.
|
|
155
|
+
protectedFromRemoval.push(withdrawnPath);
|
|
156
|
+
if (await exists(join(targetRepo, withdrawnPath))) {
|
|
157
|
+
const action = String(entry.adopter_action ?? "").trim();
|
|
158
|
+
rawWarnings.push(`[ADR ${adrId} withdrawal] ${withdrawnPath} is still present.` +
|
|
159
|
+
(action ? ` ${action}` : ""));
|
|
160
|
+
}
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
case "reassignment": {
|
|
164
|
+
const newDecision = String(entry.new_decision_path ?? "");
|
|
165
|
+
rawWarnings.push(`[ADR ${adrId} reassignment] slot now points to a different decision: ${newDecision}`);
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
default:
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const warnings = firstAdoption
|
|
173
|
+
? rawWarnings.map((w) => `${w} — HISTORICAL REPLAY: this transition predates this repo's adoption of the bundle, ` +
|
|
174
|
+
`so it is a record of what happened, not a change arriving now`)
|
|
175
|
+
: rawWarnings;
|
|
176
|
+
return { renames, warnings, protectedFromRemoval, movedPaths };
|
|
177
|
+
}
|
|
178
|
+
const CITATION_LINK = /\]\(([^)\s#]+)(?:#[^)\s]*)?\)/g;
|
|
179
|
+
/**
|
|
180
|
+
* Citations an adopter's OWN documents still make to a path that moved.
|
|
181
|
+
*
|
|
182
|
+
* A move strands its citations, not just the file: one estate held 33 citations
|
|
183
|
+
* resolving to stale copies and 25 dangling (four gate thresholds resolving to
|
|
184
|
+
* nothing) after a rubric reorg. Ruled 2026-09-29: report each, never rewrite —
|
|
185
|
+
* the quiet case (a citation that still resolves, to the wrong copy) is exactly
|
|
186
|
+
* the one an automatic rewrite could get wrong.
|
|
187
|
+
*
|
|
188
|
+
* A citation is the moved path written literally (repo-relative), or a Markdown
|
|
189
|
+
* link whose target resolves to it from the citing file. Method-managed files are
|
|
190
|
+
* excluded: the bundle already ships them corrected.
|
|
191
|
+
*/
|
|
192
|
+
export async function findStrandedCitations(targetRepo, moved, managed, trackedTextFiles) {
|
|
193
|
+
if (moved.length === 0)
|
|
194
|
+
return [];
|
|
195
|
+
const out = [];
|
|
196
|
+
for (const file of trackedTextFiles) {
|
|
197
|
+
if (managed.has(file))
|
|
198
|
+
continue;
|
|
199
|
+
let text;
|
|
200
|
+
try {
|
|
201
|
+
text = await readFile(join(targetRepo, file), "utf8");
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const lines = text.split("\n");
|
|
207
|
+
for (let i = 0; i < lines.length; i++) {
|
|
208
|
+
const line = lines[i];
|
|
209
|
+
for (const m of moved) {
|
|
210
|
+
let hit = line.includes(m.fromPath);
|
|
211
|
+
if (!hit && line.includes("](")) {
|
|
212
|
+
for (const link of line.matchAll(CITATION_LINK)) {
|
|
213
|
+
const target = decodeURIComponent(link[1]);
|
|
214
|
+
if (/^[a-z]+:/i.test(target))
|
|
215
|
+
continue;
|
|
216
|
+
if (posix.normalize(posix.join(posix.dirname(file), target)) === m.fromPath)
|
|
217
|
+
hit = true;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (hit) {
|
|
221
|
+
out.push(`${file}:${i + 1}: cites ${m.fromPath}, which moved to ${m.toPath} (${m.label}) — update the citation`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Apply the planned renames. Uses a real move so an adopter's local edits travel
|
|
230
|
+
* with the file and git can see a rename, which is the whole point of doing this
|
|
231
|
+
* rather than letting delete-plus-copy approximate it.
|
|
232
|
+
*/
|
|
233
|
+
export async function applyAdrRenames(plan, targetRepo) {
|
|
234
|
+
let applied = 0;
|
|
235
|
+
for (const r of plan.renames) {
|
|
236
|
+
const from = join(targetRepo, r.fromPath);
|
|
237
|
+
const to = join(targetRepo, r.toPath);
|
|
238
|
+
try {
|
|
239
|
+
await mkdir(dirname(to), { recursive: true });
|
|
240
|
+
await rename(from, to);
|
|
241
|
+
applied++;
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// A rename that cannot be applied is not fatal: the file-copy phase will
|
|
245
|
+
// still place the new path, leaving the old one for the adopter to resolve.
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return applied;
|
|
249
|
+
}
|
|
250
|
+
//# sourceMappingURL=adr-transitions.js.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { copyFile, mkdir, readdir, rm, rmdir } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, sep } from "node:path";
|
|
3
|
+
export async function copyManagedFiles(incomingPaths, payloadDir, targetRepo) {
|
|
4
|
+
for (const p of incomingPaths) {
|
|
5
|
+
const src = join(payloadDir, p);
|
|
6
|
+
const dst = join(targetRepo, p);
|
|
7
|
+
await mkdir(dirname(dst), { recursive: true });
|
|
8
|
+
await copyFile(src, dst);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Withdraw method-managed files, then remove the directories the withdrawal
|
|
13
|
+
* itself emptied.
|
|
14
|
+
*
|
|
15
|
+
* Without the second half, `sync` left the skeleton of everything it had ever
|
|
16
|
+
* shipped behind: measured 156 empty directories across 13 reverse-estate repos
|
|
17
|
+
* (12 `docs/method/methods/skills/reverse-*` dirs each, from the v2.3.0 wave
|
|
18
|
+
* moving reverse skills out of the forward bases). Git does not track empty
|
|
19
|
+
* directories, so no commit ever carries them and no diff ever shows them —
|
|
20
|
+
* they are invisible to review and live only in working copies, which is exactly
|
|
21
|
+
* why they accumulated unnoticed.
|
|
22
|
+
*
|
|
23
|
+
* TWO CONDITIONS, and the second is the one that matters. A directory is pruned
|
|
24
|
+
* only if it is (1) empty, and (2) DERIVED FROM THE WITHDRAWAL SET — that is, it
|
|
25
|
+
* is an ancestor directory of a path this call actually removed. Emptiness alone
|
|
26
|
+
* is not sufficient: an adopter's own empty directory that happens to sit beside
|
|
27
|
+
* the withdrawal tree is not ours to delete, and deleting it would be an
|
|
28
|
+
* unrecoverable side effect of a sync that was asked only to withdraw files.
|
|
29
|
+
* The walk stops at the first directory failing either condition, so a parent
|
|
30
|
+
* that still holds adopter content survives with its remaining entries intact —
|
|
31
|
+
* the real-world shape being 12 emptied `reverse-*` dirs beside 3 populated
|
|
32
|
+
* non-reverse skills under one `skills/` parent, where the walk must stop at
|
|
33
|
+
* `skills/`.
|
|
34
|
+
*
|
|
35
|
+
* Returns the pruned directories, repo-relative and deepest-first, so the caller
|
|
36
|
+
* reports each one. A pruned directory is an action taken on an adopter's tree,
|
|
37
|
+
* and this codebase does not take silent actions.
|
|
38
|
+
*/
|
|
39
|
+
export async function removeManagedFiles(paths, targetRepo) {
|
|
40
|
+
for (const p of paths) {
|
|
41
|
+
await rm(join(targetRepo, p), { force: true });
|
|
42
|
+
}
|
|
43
|
+
// Every directory the withdrawal set could legitimately have emptied: each
|
|
44
|
+
// removed path's ancestor chain, up to but excluding the repo root. Built
|
|
45
|
+
// from `paths` rather than from the filesystem, so nothing outside what this
|
|
46
|
+
// call withdrew is ever a candidate.
|
|
47
|
+
const candidates = new Set();
|
|
48
|
+
for (const p of paths) {
|
|
49
|
+
const parts = p.split(/[/\\]/).filter((s) => s !== "" && s !== ".");
|
|
50
|
+
for (let i = parts.length - 1; i > 0; i--) {
|
|
51
|
+
candidates.add(parts.slice(0, i).join(sep));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Deepest first, so a child is always considered before its parent and a
|
|
55
|
+
// chain of emptied directories collapses in one pass.
|
|
56
|
+
//
|
|
57
|
+
// The name tiebreak is BYTE ORDER, not `localeCompare`, and that is a contract
|
|
58
|
+
// rather than a preference: `sync-method-baseline.sh` prunes and reports the
|
|
59
|
+
// same directories, and tests/parity asserts the two produce byte-identical
|
|
60
|
+
// stdout. `localeCompare` resolves through ICU while the script's `sort`
|
|
61
|
+
// resolves through glibc — two different collation engines, both sensitive to
|
|
62
|
+
// the ambient locale, so a byte-identical contract resting on them would pass
|
|
63
|
+
// or fail by environment. The script pins `LC_ALL=C`; this pins the same order
|
|
64
|
+
// here, so the tiebreak is defined by the method and not by the machine.
|
|
65
|
+
const ordered = [...candidates].sort((a, b) => b.split(sep).length - a.split(sep).length ||
|
|
66
|
+
(a < b ? -1 : a > b ? 1 : 0));
|
|
67
|
+
const pruned = [];
|
|
68
|
+
for (const rel of ordered) {
|
|
69
|
+
const abs = join(targetRepo, rel);
|
|
70
|
+
let entries;
|
|
71
|
+
try {
|
|
72
|
+
entries = await readdir(abs);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
continue; // already gone, or never existed
|
|
76
|
+
}
|
|
77
|
+
if (entries.length > 0)
|
|
78
|
+
continue; // adopter content remains — stop here
|
|
79
|
+
try {
|
|
80
|
+
await rmdir(abs);
|
|
81
|
+
pruned.push(rel);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Racing writer, permissions, or a mount point. A directory we could not
|
|
85
|
+
// remove is left alone rather than escalated: the files are withdrawn,
|
|
86
|
+
// which is what the caller asked for.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return pruned;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=apply.js.map
|
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
* - Harness wrapper (`CLAUDE.md` / `AGENTS.md` / …) — 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, writeFile } from "node:fs/promises";
|
|
24
|
+
import { dirname, join } from "node:path";
|
|
25
|
+
import { AGENT_INSTRUCTIONS_REL_PATH, WRAPPER_BLOCK_BEGIN, WRAPPER_BLOCK_END, harnessWrapperOutputPath, renderAgentInstructions, renderHarnessWrapper, 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.
|
|
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 one almost
|
|
39
|
+
// certainly is too — skip the rest as well.
|
|
40
|
+
return { log, refused: null };
|
|
41
|
+
}
|
|
42
|
+
if (aiRender.unresolved.length > 0) {
|
|
43
|
+
log.push(`Warning: agent-instructions.md has unresolved placeholders: ${aiRender.unresolved.join(", ")}`);
|
|
44
|
+
}
|
|
45
|
+
const aiPath = join(opts.targetRepo, AGENT_INSTRUCTIONS_REL_PATH);
|
|
46
|
+
await mkdir(dirname(aiPath), { recursive: true });
|
|
47
|
+
await writeFile(aiPath, aiRender.content, "utf8");
|
|
48
|
+
log.push(`Refreshed ${AGENT_INSTRUCTIONS_REL_PATH}.`);
|
|
49
|
+
// 2. Harness wrapper — marker-aware merge.
|
|
50
|
+
const wrapperRender = await renderHarnessWrapper(opts.targetRepo, opts.agentHarness, vars);
|
|
51
|
+
if (wrapperRender === null) {
|
|
52
|
+
log.push(`Skipped ${harnessWrapperOutputPath(opts.agentHarness)}: bundle does not ship a wrapper template for ${opts.agentHarness}.`);
|
|
53
|
+
return { log, refused: null };
|
|
54
|
+
}
|
|
55
|
+
if (wrapperRender.unresolved.length > 0) {
|
|
56
|
+
log.push(`Warning: ${harnessWrapperOutputPath(opts.agentHarness)} has unresolved placeholders: ${wrapperRender.unresolved.join(", ")}`);
|
|
57
|
+
}
|
|
58
|
+
const wrapperRel = harnessWrapperOutputPath(opts.agentHarness);
|
|
59
|
+
const wrapperPath = join(opts.targetRepo, wrapperRel);
|
|
60
|
+
// Mirror the ci-workflow sync pattern: when the target file does not
|
|
61
|
+
// yet exist, write the full rendered template (block + adopter
|
|
62
|
+
// section template). When it exists, write the method-managed block
|
|
63
|
+
// and scaffold-fill blank regions around it from the template —
|
|
64
|
+
// writeMarkedBlockFile preserves non-blank adopter content verbatim.
|
|
65
|
+
const wrapperExists = await fileExists(wrapperPath);
|
|
66
|
+
const templateParts = splitWrapperTemplate(wrapperRender.content);
|
|
67
|
+
const incoming = wrapperExists
|
|
68
|
+
? templateParts.block
|
|
69
|
+
: wrapperRender.content;
|
|
70
|
+
const mergeResult = await writeMarkedBlockFile(wrapperPath, incoming, { begin: WRAPPER_BLOCK_BEGIN, end: WRAPPER_BLOCK_END }, opts.allowOverwrite, wrapperExists
|
|
71
|
+
? { pre: templateParts.pre, post: templateParts.post }
|
|
72
|
+
: undefined);
|
|
73
|
+
if (mergeResult.refused) {
|
|
74
|
+
return { log, refused: wrapperRel };
|
|
75
|
+
}
|
|
76
|
+
if (mergeResult.changed) {
|
|
77
|
+
log.push(`Refreshed ${wrapperRel}.`);
|
|
78
|
+
}
|
|
79
|
+
return { log, refused: null };
|
|
80
|
+
}
|
|
81
|
+
async function fileExists(path) {
|
|
82
|
+
try {
|
|
83
|
+
await access(path);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=briefing-merge.js.map
|