@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,208 @@
|
|
|
1
|
+
// `sdd-cli snapshot fold` — move a flat `source/<repo>@<rev>/` area into its
|
|
2
|
+
// first dated baseline (delta-reverse.md §1), once per area.
|
|
3
|
+
//
|
|
4
|
+
// Then it LISTS every reference to the old paths it can find under the scan
|
|
5
|
+
// roots — relative and absolute alike, since citations use both and a search
|
|
6
|
+
// for one form misses the other. The list is a print, not a control: nothing
|
|
7
|
+
// fails if a reference is left, so the report says so. Scan the whole estate,
|
|
8
|
+
// not only the owning SDD — other SDDs cite this area's baselines.
|
|
9
|
+
//
|
|
10
|
+
// With --sdd-path it also does what it knows how to do: each member's
|
|
11
|
+
// reverse-metadata.yaml whose source_path names a folded folder is re-pointed
|
|
12
|
+
// into the baseline (a minimal text edit), its canonical_repo_key is taken from
|
|
13
|
+
// that metadata rather than guessed from the folder name, and role_hint is
|
|
14
|
+
// derived from its profile where the method defines the mapping
|
|
15
|
+
// (`support-repo` → `ancillary`, delta-reverse.md §1) and left unset otherwise.
|
|
16
|
+
import { mkdir, readdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
17
|
+
import { join, relative, sep } from "node:path";
|
|
18
|
+
import { parse, stringify } from "yaml";
|
|
19
|
+
import { setTopLevelScalars } from "./advance.js";
|
|
20
|
+
import { SnapshotError, baselineIds, deriveKey, knownRepositoryIds, exists, isIsoDate, manifestPath, memberFolders, splitFolder, } from "./manifest.js";
|
|
21
|
+
const ROLE_BY_PROFILE = { "support-repo": "ancillary" };
|
|
22
|
+
/** The same mapping by M-ADR 0213's scope, which replaces the per-repo profile: a support repo is ancillary. */
|
|
23
|
+
const ROLE_BY_SCOPE = { support: "ancillary" };
|
|
24
|
+
/** reverse-metadata.yaml files under <sdd>/repos, by the folder their source_path names. */
|
|
25
|
+
async function pinsByFolder(sddPath) {
|
|
26
|
+
const out = new Map();
|
|
27
|
+
const repos = join(sddPath, "repos");
|
|
28
|
+
if (!(await exists(repos)))
|
|
29
|
+
return out;
|
|
30
|
+
for (const e of await readdir(repos, { withFileTypes: true })) {
|
|
31
|
+
const file = join(repos, e.name, "reverse-metadata.yaml");
|
|
32
|
+
if (!e.isDirectory() || !(await exists(file)))
|
|
33
|
+
continue;
|
|
34
|
+
const text = await readFile(file, "utf8");
|
|
35
|
+
const meta = (parse(text) ?? {});
|
|
36
|
+
const folder = String(meta.source_path ?? "").split("/").pop() || String(meta.exported_folder_name ?? "");
|
|
37
|
+
if (folder)
|
|
38
|
+
out.set(folder, { file, text, meta });
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
const SKIP = new Set([".git", "node_modules"]);
|
|
43
|
+
// Scripts cite snapshot paths too (a research extractor hard-coding an export): a
|
|
44
|
+
// scan of docs alone reported fewer references than a rewriter reading scripts.
|
|
45
|
+
const TEXT = /\.(md|ya?ml|json|jsonl|txt|adoc|csv|properties|toml|py|ts|js|mjs|sh|mermaid|puml|html)$/i;
|
|
46
|
+
/**
|
|
47
|
+
* A folder name is a PATH citation only with a `/` beside it: `source/<folder>`,
|
|
48
|
+
* `<folder>/src/…`. Bare, it is a name — `exported_folder_name: <folder>` is
|
|
49
|
+
* correct after a fold and must not be reported as stale.
|
|
50
|
+
*/
|
|
51
|
+
export function citesPath(line, folder) {
|
|
52
|
+
for (let i = line.indexOf(folder); i >= 0; i = line.indexOf(folder, i + 1)) {
|
|
53
|
+
if (line[i - 1] === "/" || line[i + folder.length] === "/")
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
async function references(root, folders, id) {
|
|
59
|
+
const out = [];
|
|
60
|
+
async function walk(dir) {
|
|
61
|
+
let entries;
|
|
62
|
+
try {
|
|
63
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
for (const e of entries) {
|
|
69
|
+
const full = join(dir, e.name);
|
|
70
|
+
if (e.isDirectory()) {
|
|
71
|
+
if (!SKIP.has(e.name) && !isIsoDate(e.name))
|
|
72
|
+
await walk(full);
|
|
73
|
+
}
|
|
74
|
+
else if (e.isFile() && TEXT.test(e.name) && (await stat(full)).size <= 4 * 1024 * 1024) {
|
|
75
|
+
const lines = (await readFile(full, "utf8")).split("\n");
|
|
76
|
+
lines.forEach((line, i) => {
|
|
77
|
+
for (const f of folders) {
|
|
78
|
+
if (citesPath(line, f) && !line.includes(`${id}/${f}`)) {
|
|
79
|
+
out.push(`${relative(root, full).split(sep).join("/")}:${i + 1}: ${f}`);
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
await walk(root);
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
export async function runSnapshotFold(inputs, write) {
|
|
91
|
+
try {
|
|
92
|
+
const { areaRoot, id } = inputs;
|
|
93
|
+
const today = inputs.today ?? new Date().toISOString().slice(0, 10);
|
|
94
|
+
if (id === undefined)
|
|
95
|
+
throw new SnapshotError("--id is required: the date the flat exports were OBSERVED");
|
|
96
|
+
if (!isIsoDate(id) || id > today)
|
|
97
|
+
throw new SnapshotError(`--id '${id}' is not a past or present YYYY-MM-DD date`);
|
|
98
|
+
if (!inputs.implementation)
|
|
99
|
+
throw new SnapshotError("--implementation is required: the durable system identity (M-ADR 0181 §2.5)");
|
|
100
|
+
const existing = await baselineIds(areaRoot);
|
|
101
|
+
for (const b of existing) {
|
|
102
|
+
if (await exists(manifestPath(areaRoot, b)))
|
|
103
|
+
throw new SnapshotError(`${areaRoot} is already folded (baseline ${b} has a manifest)`);
|
|
104
|
+
}
|
|
105
|
+
const folders = await memberFolders(areaRoot);
|
|
106
|
+
if (folders.length === 0)
|
|
107
|
+
throw new SnapshotError(`no flat <repo>@<revision>/ folders directly under ${areaRoot}`);
|
|
108
|
+
const pins = inputs.sddPath ? await pinsByFolder(inputs.sddPath) : new Map();
|
|
109
|
+
const known = inputs.sddPath ? await knownRepositoryIds(inputs.sddPath) : new Set();
|
|
110
|
+
const problems = [];
|
|
111
|
+
const unverified = [];
|
|
112
|
+
const members = folders.map((f) => {
|
|
113
|
+
const { stem, revision } = splitFolder(f);
|
|
114
|
+
const pin = pins.get(f);
|
|
115
|
+
const role = pin ? (ROLE_BY_SCOPE[String(pin.meta.repo_scope ?? "")] ?? ROLE_BY_PROFILE[String(pin.meta.profile ?? "")]) : undefined;
|
|
116
|
+
let key = inputs.keys?.get(f) ?? (pin?.meta.canonical_repo_key ? String(pin.meta.canonical_repo_key) : undefined);
|
|
117
|
+
if (key === undefined) {
|
|
118
|
+
const d = deriveKey(f, stem, known);
|
|
119
|
+
key = d.key;
|
|
120
|
+
if (d.problem)
|
|
121
|
+
problems.push(d.problem);
|
|
122
|
+
if (d.unverified)
|
|
123
|
+
unverified.push(`${f} -> '${d.key}'`);
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
canonical_repo_key: key,
|
|
127
|
+
exported_folder_name: f,
|
|
128
|
+
source_revision: revision,
|
|
129
|
+
...(role ? { role_hint: role } : {}),
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
// Refused before anything moves: a key written wrong is a record every later
|
|
133
|
+
// verb, binding and catalogue inherits.
|
|
134
|
+
if (problems.length > 0)
|
|
135
|
+
throw new SnapshotError(`${problems.length} derived key(s) are not repository ids:\n - ${problems.join("\n - ")}`);
|
|
136
|
+
for (const u of unverified)
|
|
137
|
+
write(`UNVERIFIED key (no --sdd-path, so no repository ids to check against): ${u}\n`);
|
|
138
|
+
const target = join(areaRoot, id);
|
|
139
|
+
if (await exists(target))
|
|
140
|
+
throw new SnapshotError(`${target} already exists`);
|
|
141
|
+
await mkdir(target);
|
|
142
|
+
for (const f of folders)
|
|
143
|
+
await rename(join(areaRoot, f), join(target, f));
|
|
144
|
+
const manifest = { snapshot_id: id, observed_at: id, implementation: inputs.implementation, prior_snapshot: null, members };
|
|
145
|
+
await writeFile(manifestPath(areaRoot, id), "# Implementation-set snapshot (baseline) -- written by `sdd-cli snapshot fold`.\n" + stringify(manifest, { lineWidth: 0 }));
|
|
146
|
+
write(`folded ${folders.length} member(s) into ${target}\n`);
|
|
147
|
+
if (inputs.sddPath) {
|
|
148
|
+
let repointed = 0;
|
|
149
|
+
for (const f of folders) {
|
|
150
|
+
const pin = pins.get(f);
|
|
151
|
+
if (!pin) {
|
|
152
|
+
write(` no reverse-metadata.yaml in ${inputs.sddPath}/repos names ${f} — its evidence pin is not re-pointed\n`);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const sp = String(pin.meta.source_path ?? "");
|
|
156
|
+
const parts = sp.split("/");
|
|
157
|
+
if (parts.some((p) => isIsoDate(p)))
|
|
158
|
+
continue;
|
|
159
|
+
const next = [...parts.slice(0, -1), id, parts[parts.length - 1]].join("/");
|
|
160
|
+
const fields = { source_path: next };
|
|
161
|
+
if (!pin.meta.exported_folder_name)
|
|
162
|
+
fields.exported_folder_name = f;
|
|
163
|
+
await writeFile(pin.file, setTopLevelScalars(pin.text, fields));
|
|
164
|
+
repointed++;
|
|
165
|
+
}
|
|
166
|
+
write(`re-pointed ${repointed} evidence pin(s) in ${inputs.sddPath} into ${id}/\n`);
|
|
167
|
+
// A pin that names a folder this area does NOT hold — under the same area prefix
|
|
168
|
+
// as the pins that matched — was stale BEFORE the fold (its export moved or was
|
|
169
|
+
// never here). Inserting a date would not make it true, so it is listed, not touched.
|
|
170
|
+
const prefixes = new Set(folders.flatMap((f) => {
|
|
171
|
+
const sp = String(pins.get(f)?.meta.source_path ?? "");
|
|
172
|
+
return sp ? [sp.split("/").slice(0, -1).filter((p) => !isIsoDate(p)).join("/")] : [];
|
|
173
|
+
}));
|
|
174
|
+
const stale = [...pins.entries()].filter(([folder, pin]) => {
|
|
175
|
+
if (folders.includes(folder))
|
|
176
|
+
return false;
|
|
177
|
+
const parts = String(pin.meta.source_path ?? "").split("/");
|
|
178
|
+
return !parts.some((p) => isIsoDate(p)) && prefixes.has(parts.slice(0, -1).join("/"));
|
|
179
|
+
});
|
|
180
|
+
for (const [folder, pin] of stale) {
|
|
181
|
+
write(` STALE PIN (not re-pointed): ${relative(inputs.sddPath, pin.file)} names ${folder}, which this area does not hold\n`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const roots = inputs.scanRoots ?? [];
|
|
185
|
+
if (roots.length === 0) {
|
|
186
|
+
write("WARNING: no --scan roots given, so no references were listed. Every citation of the old paths is now stale.\n");
|
|
187
|
+
return { exitCode: 0 };
|
|
188
|
+
}
|
|
189
|
+
let n = 0;
|
|
190
|
+
for (const r of roots) {
|
|
191
|
+
for (const hit of await references(r, folders, id)) {
|
|
192
|
+
write(` ${r}/${hit}\n`);
|
|
193
|
+
n++;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
write(`${n} line(s) still cite a pre-fold path. This list is a print, not a control: nothing fails if one is left — ` +
|
|
197
|
+
"re-point each (reverse-metadata source_path first) and check it resolves on disk.\n");
|
|
198
|
+
return { exitCode: 0 };
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
if (e instanceof SnapshotError) {
|
|
202
|
+
write(`REFUSED: ${e.message}\n`);
|
|
203
|
+
return { exitCode: 1 };
|
|
204
|
+
}
|
|
205
|
+
throw e;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=fold.js.map
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// `sdd-cli snapshot land` — write T1's set manifest (delta-reverse.md §2).
|
|
2
|
+
//
|
|
3
|
+
// Refusals, each a failure a hand run actually made:
|
|
4
|
+
// - no --id, or an id in the future: the baseline id is the OBSERVATION date and
|
|
5
|
+
// has no default, so the processing date cannot slip in unasked;
|
|
6
|
+
// - a member observed after the set date;
|
|
7
|
+
// - a prior member absent from the export and not declared carried (with a
|
|
8
|
+
// reason) or removed (with a reason) — never left to be inferred;
|
|
9
|
+
// - a member re-exported at the same revision whose bytes differ;
|
|
10
|
+
// - no recorded credential scan, or one with findings but no disposition —
|
|
11
|
+
// a snapshot owner often cannot remediate upstream code, so findings land
|
|
12
|
+
// with the disposition on the record, never as a false `clean`;
|
|
13
|
+
// - an existing snapshot.yaml (no overwrite).
|
|
14
|
+
import { writeFile } from "node:fs/promises";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { stringify } from "yaml";
|
|
17
|
+
import { SnapshotError, baselineIds, deriveKey, diffTrees, exists, hashTree, isIsoDate, knownRepositoryIds, manifestPath, memberBytesDir, memberFolders, readManifest, splitFolder, } from "./manifest.js";
|
|
18
|
+
export async function runSnapshotLand(inputs, write) {
|
|
19
|
+
try {
|
|
20
|
+
const m = await land(inputs, write);
|
|
21
|
+
write(`landed ${m.snapshot_id}: ${m.members.length} member(s), ${m.members_removed?.length ?? 0} removed\n`);
|
|
22
|
+
return { exitCode: 0 };
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
if (e instanceof SnapshotError) {
|
|
26
|
+
write(`REFUSED: ${e.message}\n`);
|
|
27
|
+
return { exitCode: 1 };
|
|
28
|
+
}
|
|
29
|
+
throw e;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function land(inputs, write) {
|
|
33
|
+
const { areaRoot } = inputs;
|
|
34
|
+
const today = inputs.today ?? new Date().toISOString().slice(0, 10);
|
|
35
|
+
const id = inputs.id;
|
|
36
|
+
if (id === undefined) {
|
|
37
|
+
throw new SnapshotError("--id is required: the date the snapshot was OBSERVED. It has no default, so the date it was processed cannot stand in for it.");
|
|
38
|
+
}
|
|
39
|
+
if (!isIsoDate(id))
|
|
40
|
+
throw new SnapshotError(`--id '${id}' is not a YYYY-MM-DD date`);
|
|
41
|
+
if (id > today)
|
|
42
|
+
throw new SnapshotError(`--id ${id} is in the future (today is ${today})`);
|
|
43
|
+
const dir = join(areaRoot, id);
|
|
44
|
+
if (!(await exists(dir)))
|
|
45
|
+
throw new SnapshotError(`no baseline folder ${dir} — export the members into it first`);
|
|
46
|
+
if (await exists(manifestPath(areaRoot, id))) {
|
|
47
|
+
throw new SnapshotError(`${manifestPath(areaRoot, id)} already exists — land does not overwrite a manifest`);
|
|
48
|
+
}
|
|
49
|
+
const priorId = inputs.prior ?? (await baselineIds(areaRoot)).filter((b) => b < id).pop();
|
|
50
|
+
const prior = priorId === undefined ? null : await readManifest(areaRoot, priorId);
|
|
51
|
+
const implementation = inputs.implementation ?? prior?.implementation;
|
|
52
|
+
if (!implementation)
|
|
53
|
+
throw new SnapshotError("--implementation is required when there is no prior baseline");
|
|
54
|
+
const scan = inputs.credentialScan;
|
|
55
|
+
if (!scan || !scan.tool || !scan.version || !scan.date || !scan.result) {
|
|
56
|
+
throw new SnapshotError("a credential scan must be recorded before anything reads the tree: --credential-scan tool=…,version=…,date=YYYY-MM-DD,result=clean");
|
|
57
|
+
}
|
|
58
|
+
if (!isIsoDate(scan.date))
|
|
59
|
+
throw new SnapshotError(`credential scan date '${scan.date}' is not YYYY-MM-DD`);
|
|
60
|
+
if (scan.result !== "clean" && !scan.disposition) {
|
|
61
|
+
throw new SnapshotError(`credential scan result is '${scan.result}': record a disposition=… (what was found, and why it stands — e.g. present at the prior baseline, upstream's to rotate). ` +
|
|
62
|
+
"Never record `clean` for a scan that found something.");
|
|
63
|
+
}
|
|
64
|
+
if (scan.result !== "clean")
|
|
65
|
+
write(`CREDENTIAL FINDINGS landing on the record: ${scan.result} — ${scan.disposition}\n`);
|
|
66
|
+
const priorByKey = new Map((prior?.members ?? []).map((m) => [m.canonical_repo_key, m]));
|
|
67
|
+
const priorByStem = new Map((prior?.members ?? []).flatMap((m) => {
|
|
68
|
+
const s = splitFolder(m.exported_folder_name);
|
|
69
|
+
return s ? [[s.stem, m.canonical_repo_key]] : [];
|
|
70
|
+
}));
|
|
71
|
+
const known = inputs.sddPath ? await knownRepositoryIds(inputs.sddPath) : new Set();
|
|
72
|
+
const keyProblems = [];
|
|
73
|
+
const members = [];
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
for (const folder of await memberFolders(dir)) {
|
|
76
|
+
const { stem, revision } = splitFolder(folder);
|
|
77
|
+
let key = inputs.keys?.get(folder) ?? priorByStem.get(stem);
|
|
78
|
+
if (key === undefined) {
|
|
79
|
+
const d = deriveKey(folder, stem, known);
|
|
80
|
+
key = d.key;
|
|
81
|
+
if (d.problem) {
|
|
82
|
+
keyProblems.push(d.problem);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
write(`note: ${folder} is new; canonical_repo_key derived as '${key}'${d.unverified ? " — UNVERIFIED (no --sdd-path)" : ""} (override with --key ${folder}=<key>)\n`);
|
|
86
|
+
}
|
|
87
|
+
if (seen.has(key))
|
|
88
|
+
throw new SnapshotError(`two member folders resolve to canonical_repo_key '${key}'`);
|
|
89
|
+
seen.add(key);
|
|
90
|
+
const entry = { canonical_repo_key: key, exported_folder_name: folder, source_revision: revision };
|
|
91
|
+
const role = inputs.roleHints?.get(key) ?? priorByKey.get(key)?.role_hint;
|
|
92
|
+
if (role)
|
|
93
|
+
entry.role_hint = role;
|
|
94
|
+
const md = inputs.memberDates?.get(key);
|
|
95
|
+
if (md !== undefined) {
|
|
96
|
+
if (!isIsoDate(md))
|
|
97
|
+
throw new SnapshotError(`--member-date ${key}=${md} is not YYYY-MM-DD`);
|
|
98
|
+
if (md > id)
|
|
99
|
+
throw new SnapshotError(`member ${key} observed ${md}, after the set date ${id}`);
|
|
100
|
+
if (md !== id)
|
|
101
|
+
entry.observed_at = md;
|
|
102
|
+
}
|
|
103
|
+
const p = priorByKey.get(key);
|
|
104
|
+
if (prior && p && p.source_revision === revision) {
|
|
105
|
+
const before = await hashTree(memberBytesDir(areaRoot, prior, p));
|
|
106
|
+
const after = await hashTree(join(dir, folder));
|
|
107
|
+
const d = diffTrees(before, after);
|
|
108
|
+
const changed = d.added.length + d.removed.length + d.modified.length;
|
|
109
|
+
if (changed > 0) {
|
|
110
|
+
throw new SnapshotError(`${key} was re-exported at the same revision ${revision} as ${prior.snapshot_id}, but ${changed} file(s) differ — one of the two exports is not what its name says`);
|
|
111
|
+
}
|
|
112
|
+
entry.reverified_at = md ?? id;
|
|
113
|
+
entry.reverification = `re-exported at the same revision as ${prior.snapshot_id}; ${after.size} file(s) byte-identical`;
|
|
114
|
+
}
|
|
115
|
+
members.push(entry);
|
|
116
|
+
}
|
|
117
|
+
if (keyProblems.length > 0) {
|
|
118
|
+
throw new SnapshotError(`${keyProblems.length} derived key(s) are not repository ids:\n - ${keyProblems.join("\n - ")}`);
|
|
119
|
+
}
|
|
120
|
+
const removed = [];
|
|
121
|
+
const unaccounted = [];
|
|
122
|
+
for (const p of prior?.members ?? []) {
|
|
123
|
+
if (seen.has(p.canonical_repo_key))
|
|
124
|
+
continue;
|
|
125
|
+
const reasonCarried = inputs.carried?.get(p.canonical_repo_key);
|
|
126
|
+
const reasonRemoved = inputs.removed?.get(p.canonical_repo_key);
|
|
127
|
+
if (reasonCarried && reasonRemoved) {
|
|
128
|
+
throw new SnapshotError(`${p.canonical_repo_key} is declared both carried and removed`);
|
|
129
|
+
}
|
|
130
|
+
if (reasonCarried) {
|
|
131
|
+
members.push({
|
|
132
|
+
canonical_repo_key: p.canonical_repo_key,
|
|
133
|
+
exported_folder_name: p.exported_folder_name,
|
|
134
|
+
source_revision: p.source_revision,
|
|
135
|
+
carried_from: p.carried_from ?? prior.snapshot_id,
|
|
136
|
+
carried_reason: reasonCarried,
|
|
137
|
+
...(p.role_hint ? { role_hint: p.role_hint } : {}),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
else if (reasonRemoved) {
|
|
141
|
+
removed.push({
|
|
142
|
+
canonical_repo_key: p.canonical_repo_key,
|
|
143
|
+
last_revision: p.source_revision,
|
|
144
|
+
last_observed: p.carried_from ?? prior.snapshot_id,
|
|
145
|
+
reason: reasonRemoved,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
unaccounted.push(p.canonical_repo_key);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (unaccounted.length > 0) {
|
|
153
|
+
throw new SnapshotError(`${unaccounted.length} member(s) of ${priorId} are absent from this export and not accounted for: ${unaccounted.join(", ")}.\n` +
|
|
154
|
+
" Declare each --carried <key>=<why it was not re-observed> or --removed <key>=<why it left>. " +
|
|
155
|
+
"An absence nobody can explain yet is an open question: carry it with that as its reason, and never let it be inferred.");
|
|
156
|
+
}
|
|
157
|
+
for (const [flag, map] of [["--carried", inputs.carried], ["--removed", inputs.removed]]) {
|
|
158
|
+
for (const k of map?.keys() ?? []) {
|
|
159
|
+
if (!priorByKey.has(k) || seen.has(k))
|
|
160
|
+
throw new SnapshotError(`${flag} ${k}: not a member of ${priorId ?? "the prior baseline"} that is absent from this export`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const manifest = {
|
|
164
|
+
snapshot_id: id,
|
|
165
|
+
observed_at: id,
|
|
166
|
+
implementation,
|
|
167
|
+
prior_snapshot: priorId ?? null,
|
|
168
|
+
members,
|
|
169
|
+
...(removed.length > 0 ? { members_removed: removed } : {}),
|
|
170
|
+
credential_scan: scan,
|
|
171
|
+
...(inputs.notes ? { notes: inputs.notes } : {}),
|
|
172
|
+
};
|
|
173
|
+
const header = "# Implementation-set snapshot (baseline) -- reverse/artifact-conventions.md \"Implementation-Set Snapshots\".\n" +
|
|
174
|
+
"# Written by `sdd-cli snapshot land`.\n";
|
|
175
|
+
await writeFile(manifestPath(areaRoot, id), header + stringify(manifest, { lineWidth: 0 }));
|
|
176
|
+
return manifest;
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=land.js.map
|
|
@@ -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
|