@sdd-method/maintain-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/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 +220 -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 +98 -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/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 +818 -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 +110 -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,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.claude/settings.json` merge pass during `sdd-cli sync`. Method-
|
|
3
|
+
* managed `mcpServers` entries (see src/lib/mcp-seed/) get upserted;
|
|
4
|
+
* any adopter-added entries or other top-level keys are preserved.
|
|
5
|
+
*
|
|
6
|
+
* The file cannot ship in the method baseline bundle because it
|
|
7
|
+
* carries per-adopter absolute paths. That's why this is a separate
|
|
8
|
+
* sync step, outside the bundle-driven `copyManagedFiles` path.
|
|
9
|
+
*
|
|
10
|
+
* Conflict handling mirrors ADR 0114: if the adopter has manually
|
|
11
|
+
* modified a method-owned entry, sync refuses unless
|
|
12
|
+
* `--allow-overwrite` is passed.
|
|
13
|
+
*/
|
|
14
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
15
|
+
import { dirname, join, resolve } from "node:path";
|
|
16
|
+
import { buildMethodManagedServers, detectManagedDrift, upsertMethodManagedServers, } from "../mcp-seed/index.js";
|
|
17
|
+
import { buildMethodManagedHooks, detectHooksDrift, upsertMethodManagedHooks, } from "../hooks-seed/index.js";
|
|
18
|
+
export const SETTINGS_REL_PATH = ".claude/settings.json";
|
|
19
|
+
export async function syncMethodManagedSettings(opts) {
|
|
20
|
+
const absPath = join(opts.targetRepo, SETTINGS_REL_PATH);
|
|
21
|
+
const existing = await readSettings(absPath);
|
|
22
|
+
// No profile gets an SDD_REPO_PATH any more (ruled 2026-10-01). It was written for platform
|
|
23
|
+
// repos only because the resolver treated a marker-bearing repo as one candidate among its
|
|
24
|
+
// marker-bearing siblings (MULTIPLE_SDDS_FOUND in the reverse estate's layout). The resolver now
|
|
25
|
+
// lets a platform-product repo serve itself, so the env var, an absolute, machine-specific
|
|
26
|
+
// path in a COMMITTED file, is no longer needed. A value left by an older sync that points at
|
|
27
|
+
// the repo itself is this tool's own output and is removed (normaliseSddRepoPathForDrift); a
|
|
28
|
+
// value pointing anywhere else is the adopter's and is still drift. App/integration repos
|
|
29
|
+
// configure their sibling platform SDD as before.
|
|
30
|
+
const owned = buildMethodManagedServers({});
|
|
31
|
+
// Drift is detected against a PATH-NORMALISED view of the existing file,
|
|
32
|
+
// not the raw bytes. Reason, measured on a live estate: the F27 defect
|
|
33
|
+
// wrote the literal CLI argument (".") as SDD_REPO_PATH. Once the
|
|
34
|
+
// generator was fixed to emit the absolute form, a strict comparison
|
|
35
|
+
// classified the defect's own historical output as an ADOPTER
|
|
36
|
+
// modification and refused to correct it — while the only offered remedy,
|
|
37
|
+
// --allow-overwrite, is the flag that rewrites genuinely adopter-owned
|
|
38
|
+
// keys. A repair that requires the dangerous flag is not a repair. So: an
|
|
39
|
+
// existing SDD_REPO_PATH that RESOLVES (from the target repo) to the same
|
|
40
|
+
// absolute path as the generated value is the tool's own output in an
|
|
41
|
+
// older costume, and is treated as regenerable, never as drift. The raw
|
|
42
|
+
// `existing` still flows into the upsert below, so the merge rewrites the
|
|
43
|
+
// stale form to the canonical one — the sync self-heals instead of
|
|
44
|
+
// wedging. A path that resolves DIFFERENTLY is still drift.
|
|
45
|
+
const drifted = detectManagedDrift(normaliseSddRepoPathForDrift(existing, owned, opts.targetRepo), owned);
|
|
46
|
+
// Computed from the state as READ, before any upsert: which method-managed ids
|
|
47
|
+
// were absent from the file this run opened. Combined with `hadPreviousSync`
|
|
48
|
+
// that is a re-seed; without it, a first seeding. Deliberately not derived
|
|
49
|
+
// from the upsert's `added`, which cannot tell the two apart — that conflation
|
|
50
|
+
// IS the defect.
|
|
51
|
+
const absentOnRead = absentManagedIds(existing, owned);
|
|
52
|
+
const reseeded = opts.hadPreviousSync === true ? absentOnRead : [];
|
|
53
|
+
const hooksActive = opts.agentHarness === "claude-code";
|
|
54
|
+
const ownedHooks = hooksActive ? buildMethodManagedHooks() : undefined;
|
|
55
|
+
const hooksDrifted = hooksActive && ownedHooks
|
|
56
|
+
? detectHooksDrift(existing, ownedHooks)
|
|
57
|
+
: [];
|
|
58
|
+
if ((drifted.length > 0 || hooksDrifted.length > 0) && !opts.allowOverwrite) {
|
|
59
|
+
return {
|
|
60
|
+
path: SETTINGS_REL_PATH,
|
|
61
|
+
drifted,
|
|
62
|
+
added: [],
|
|
63
|
+
// Reported even on the refusal path: the re-seed was OBSERVED when the
|
|
64
|
+
// file was read, and it stays true whether or not this run went on to
|
|
65
|
+
// write. Zeroing it here would make a refusal hide a second finding.
|
|
66
|
+
reseeded,
|
|
67
|
+
updated: [],
|
|
68
|
+
unchanged: [],
|
|
69
|
+
hooksChangedEvents: [],
|
|
70
|
+
hooksDrifted,
|
|
71
|
+
wroteFile: false,
|
|
72
|
+
refused: true,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (opts.checkOnly) {
|
|
76
|
+
return {
|
|
77
|
+
path: SETTINGS_REL_PATH,
|
|
78
|
+
drifted,
|
|
79
|
+
added: [],
|
|
80
|
+
reseeded,
|
|
81
|
+
updated: [],
|
|
82
|
+
unchanged: [],
|
|
83
|
+
hooksChangedEvents: [],
|
|
84
|
+
hooksDrifted,
|
|
85
|
+
wroteFile: false,
|
|
86
|
+
refused: false,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const mcpResult = upsertMethodManagedServers(existing, owned);
|
|
90
|
+
let nextSettings = mcpResult.settings;
|
|
91
|
+
let hooksChangedEvents = [];
|
|
92
|
+
if (hooksActive && ownedHooks) {
|
|
93
|
+
const hooksResult = upsertMethodManagedHooks(nextSettings, ownedHooks);
|
|
94
|
+
nextSettings = hooksResult.settings;
|
|
95
|
+
hooksChangedEvents = [...hooksResult.changedEvents];
|
|
96
|
+
}
|
|
97
|
+
const changed = mcpResult.changed || hooksChangedEvents.length > 0;
|
|
98
|
+
if (!changed) {
|
|
99
|
+
return {
|
|
100
|
+
path: SETTINGS_REL_PATH,
|
|
101
|
+
drifted,
|
|
102
|
+
added: mcpResult.added,
|
|
103
|
+
reseeded,
|
|
104
|
+
updated: mcpResult.updated,
|
|
105
|
+
unchanged: mcpResult.unchanged,
|
|
106
|
+
hooksChangedEvents: [],
|
|
107
|
+
hooksDrifted,
|
|
108
|
+
wroteFile: false,
|
|
109
|
+
refused: false,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
113
|
+
await writeFile(absPath, JSON.stringify(nextSettings, null, 2) + "\n", "utf8");
|
|
114
|
+
return {
|
|
115
|
+
path: SETTINGS_REL_PATH,
|
|
116
|
+
drifted,
|
|
117
|
+
added: mcpResult.added,
|
|
118
|
+
reseeded,
|
|
119
|
+
updated: mcpResult.updated,
|
|
120
|
+
unchanged: mcpResult.unchanged,
|
|
121
|
+
hooksChangedEvents,
|
|
122
|
+
hooksDrifted,
|
|
123
|
+
wroteFile: true,
|
|
124
|
+
refused: false,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Method-managed ids absent from the settings file as read.
|
|
129
|
+
*
|
|
130
|
+
* A missing or malformed `mcpServers` map counts every managed id as absent —
|
|
131
|
+
* `readSettings` already collapses an unparseable file to `{}`, and a file whose
|
|
132
|
+
* managed block was destroyed rather than edited is exactly the re-seed case
|
|
133
|
+
* this exists to name.
|
|
134
|
+
*/
|
|
135
|
+
function absentManagedIds(existing, owned) {
|
|
136
|
+
const servers = existing["mcpServers"];
|
|
137
|
+
const present = servers && typeof servers === "object" && !Array.isArray(servers)
|
|
138
|
+
? servers
|
|
139
|
+
: {};
|
|
140
|
+
return Object.keys(owned)
|
|
141
|
+
.filter((id) => !(id in present))
|
|
142
|
+
.sort();
|
|
143
|
+
}
|
|
144
|
+
async function readSettings(absPath) {
|
|
145
|
+
if (!(await fileExists(absPath)))
|
|
146
|
+
return {};
|
|
147
|
+
const raw = await readFile(absPath, "utf8");
|
|
148
|
+
if (raw.trim().length === 0)
|
|
149
|
+
return {};
|
|
150
|
+
try {
|
|
151
|
+
const parsed = JSON.parse(raw);
|
|
152
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
153
|
+
return parsed;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
/* fall through — treat malformed as empty so we at least seed method content */
|
|
158
|
+
}
|
|
159
|
+
return {};
|
|
160
|
+
}
|
|
161
|
+
async function fileExists(path) {
|
|
162
|
+
try {
|
|
163
|
+
await access(path);
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* A view of `existing` for drift detection ONLY: where a method-managed
|
|
172
|
+
* entry's `env.SDD_REPO_PATH` resolves (from the target repo) to the same
|
|
173
|
+
* absolute path as the generated value, substitute the generated form.
|
|
174
|
+
*
|
|
175
|
+
* This makes "the tool's own older output" (the F27 relative-path defect,
|
|
176
|
+
* or a lexically different spelling of the same location) compare as
|
|
177
|
+
* pristine, so the merge proceeds and REWRITES it — while a path that
|
|
178
|
+
* resolves somewhere else entirely remains drift and still refuses.
|
|
179
|
+
* Never used for the write: the raw `existing` flows into the upsert, so
|
|
180
|
+
* the correction actually lands on disk.
|
|
181
|
+
*/
|
|
182
|
+
function normaliseSddRepoPathForDrift(existing, owned, targetRepo) {
|
|
183
|
+
const servers = existing["mcpServers"];
|
|
184
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
|
|
185
|
+
return existing;
|
|
186
|
+
}
|
|
187
|
+
const current = servers;
|
|
188
|
+
const out = {};
|
|
189
|
+
let touched = false;
|
|
190
|
+
for (const [id, entry] of Object.entries(current)) {
|
|
191
|
+
const existingPath = entry?.env?.["SDD_REPO_PATH"];
|
|
192
|
+
const ownedPath = owned[id]?.env?.["SDD_REPO_PATH"];
|
|
193
|
+
if (typeof existingPath === "string" &&
|
|
194
|
+
ownedPath === undefined &&
|
|
195
|
+
resolve(targetRepo, existingPath) === resolve(targetRepo)) {
|
|
196
|
+
// The retired platform SDD_REPO_PATH, pointing at the repo itself: regenerable, so the
|
|
197
|
+
// drift view drops it and the write (which replaces the entry whole) removes it.
|
|
198
|
+
const { SDD_REPO_PATH: _dropped, ...rest } = entry.env ?? {};
|
|
199
|
+
const { env: _env, ...bare } = entry;
|
|
200
|
+
out[id] = Object.keys(rest).length > 0 ? { ...bare, env: rest } : bare;
|
|
201
|
+
touched = true;
|
|
202
|
+
}
|
|
203
|
+
else if (typeof existingPath === "string" &&
|
|
204
|
+
typeof ownedPath === "string" &&
|
|
205
|
+
existingPath !== ownedPath &&
|
|
206
|
+
resolve(targetRepo, existingPath) === resolve(ownedPath)) {
|
|
207
|
+
out[id] = { ...entry, env: { ...entry.env, SDD_REPO_PATH: ownedPath } };
|
|
208
|
+
touched = true;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
out[id] = entry;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return touched ? { ...existing, mcpServers: out } : existing;
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=settings-merge.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
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Profile → bundle coherence (A2 of the bundle-migration plan).
|
|
2
|
+
//
|
|
3
|
+
// A repo's declared profile (`.sdd/sdd-manifest.yaml`) and the forward base
|
|
4
|
+
// its lock carries can silently disagree: a reclassification edits the
|
|
5
|
+
// manifest, nothing re-syncs the bundle, and no instrument notices — the
|
|
6
|
+
// registration/adoption-lag class. Measured instance: two platform-subsystem
|
|
7
|
+
// repos carried app-baseline for eight days, found by a human sweep.
|
|
8
|
+
//
|
|
9
|
+
// REPORTING-ONLY: a mismatch prints a [REPORT] line with the migration path
|
|
10
|
+
// and exits 0. Gating is a later, deliberate change-set once both estates
|
|
11
|
+
// read 0 (the scores-coverage precedent, M-ADR 0205 §4.5).
|
|
12
|
+
import { readLockFile, findForwardBase } from "../lock-file.js";
|
|
13
|
+
import { loadSddManifest, } from "../catalogue/manifest-loader.js";
|
|
14
|
+
import { getSddCliVersion } from "../package-version.js";
|
|
15
|
+
/**
|
|
16
|
+
* Manifest profile → the forward base that profile expects. Profiles with no
|
|
17
|
+
* entry (support-repo, organisation-profile) carry no expectation — silence,
|
|
18
|
+
* not a guess. NOTE: platform-subsystem is a CATEGORY under platform-profile,
|
|
19
|
+
* so the two measured mismatch repos resolve through the platform-profile row.
|
|
20
|
+
*/
|
|
21
|
+
const EXPECTED_FORWARD_BASE = {
|
|
22
|
+
"platform-profile": "platform-baseline",
|
|
23
|
+
"application-product": "app-baseline",
|
|
24
|
+
"integration-product": "integration-baseline",
|
|
25
|
+
};
|
|
26
|
+
export async function checkBundleCoherence(targetRepo) {
|
|
27
|
+
const lines = [];
|
|
28
|
+
const lock = await readLockFile(targetRepo);
|
|
29
|
+
const forwardBase = findForwardBase(lock);
|
|
30
|
+
if (!forwardBase) {
|
|
31
|
+
return {
|
|
32
|
+
exitCode: 0,
|
|
33
|
+
stdout: "",
|
|
34
|
+
skipped: "no forward base in method-baseline.lock — nothing to check",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
let profile;
|
|
38
|
+
try {
|
|
39
|
+
const manifest = await loadSddManifest(targetRepo, {
|
|
40
|
+
cliVersion: getSddCliVersion(),
|
|
41
|
+
});
|
|
42
|
+
profile = manifest.profile;
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
// Reporting-only: an absent/invalid manifest is its own (already-gated)
|
|
46
|
+
// problem; this check must not double-report it as a bundle mismatch.
|
|
47
|
+
return {
|
|
48
|
+
exitCode: 0,
|
|
49
|
+
stdout: "",
|
|
50
|
+
skipped: `manifest unavailable (${err.message.split("\n")[0]})`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const expected = EXPECTED_FORWARD_BASE[profile];
|
|
54
|
+
if (expected === undefined) {
|
|
55
|
+
// Nothing to compare is not a pass: counting it as one inflated the
|
|
56
|
+
// suite's "passed" figure for profiles that carry no expectation.
|
|
57
|
+
return {
|
|
58
|
+
exitCode: 0,
|
|
59
|
+
stdout: "",
|
|
60
|
+
skipped: `profile '${profile}' declares no forward-base expectation — nothing to compare`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (forwardBase.name === expected) {
|
|
64
|
+
lines.push(`[PASS] bundle-coherence: profile '${profile}' ↔ forward base '${forwardBase.name}'\n`);
|
|
65
|
+
return { exitCode: 0, stdout: lines.join("") };
|
|
66
|
+
}
|
|
67
|
+
lines.push(`[REPORT] bundle-coherence: profile '${profile}' expects forward base '${expected}' ` +
|
|
68
|
+
`but the lock carries '${forwardBase.name}' — profile and bundle disagree. ` +
|
|
69
|
+
`Migration path: sdd-cli sync --bundle <${expected} tarball> --replace-bundle ${forwardBase.name} --dry-run, ` +
|
|
70
|
+
`review the three-way split, then re-run with --yes. ` +
|
|
71
|
+
`(Reporting-only; this check does not gate.)\n`);
|
|
72
|
+
return { exitCode: 0, stdout: lines.join("") };
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=bundle-coherence.js.map
|