@sdd-method/maintain-cli 0.1.0 → 0.2.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/README.md +1 -1
- package/dist/lib/agent-harness.js +17 -8
- package/dist/lib/briefing/index.js +1 -1
- package/dist/lib/briefing/render.js +17 -8
- package/dist/lib/catalogue/aggregation-manifest-loader.js +4 -4
- package/dist/lib/catalogue/freshness.js +4 -3
- package/dist/lib/context-pull/index.js +79 -39
- package/dist/lib/context-pull/retired-mcp-setup.js +7 -0
- package/dist/lib/hooks-seed/entries.js +30 -1
- package/dist/lib/hooks-seed/index.js +4 -7
- package/dist/lib/hooks-seed/merge.js +31 -0
- package/dist/lib/sync/briefing-merge.js +50 -34
- package/dist/lib/sync/diff.js +1 -1
- package/dist/lib/sync/index.js +20 -70
- package/dist/lib/sync/settings-merge.js +70 -162
- package/dist/maintain-program.js +5 -2
- package/dist/maintain-version.json +2 -2
- package/dist/verbs/catalogue-check.js +28 -0
- package/dist/verbs/context.js +1 -1
- package/dist/verify-program.js +1 -1
- package/dist/verify-version.json +2 -2
- package/package.json +2 -2
- package/dist/lib/hooks-seed/copilot.js +0 -167
- package/dist/lib/hooks-seed/lifecycle-mapping.js +0 -28
- package/dist/lib/mcp-seed/entries.js +0 -14
- package/dist/lib/mcp-seed/index.js +0 -28
- package/dist/lib/mcp-seed/merge.js +0 -99
- package/dist/lib/mcp-seed/types.js +0 -13
- package/dist/lib/sync/copilot-hooks-merge.js +0 -108
package/dist/lib/sync/index.js
CHANGED
|
@@ -11,12 +11,11 @@ import { cacheBundle, ensureCacheGitignored, splitConsumptionModes, } from "./bu
|
|
|
11
11
|
import { PROTECTED_PREFIXES, allPristine, classifyConflicts, computeRemovals, detectShadowCollisions, digestManagedPaths, findConflicts, findMissingPayloadFiles, findProtectedPaths, } from "./diff.js";
|
|
12
12
|
import { applyAdrRenames, findStrandedCitations, planAdrTransitions, } from "./adr-transitions.js";
|
|
13
13
|
import { syncMethodManagedSettings } from "./settings-merge.js";
|
|
14
|
-
import { syncMethodManagedCopilotHooks } from "./copilot-hooks-merge.js";
|
|
15
14
|
import { syncBriefingFiles } from "./briefing-merge.js";
|
|
16
15
|
import { DEFAULT_AGENT_HARNESS } from "../agent-harness.js";
|
|
17
16
|
import { BLOCK_BEGIN, BLOCK_END } from "../init/ci-workflow.js";
|
|
18
17
|
import { blockSha256, CI_BINDINGS, DEFAULT_CI_BINDING, isCiBinding, parseCiRunner, syncCiBinding, } from "../ci-binding/index.js";
|
|
19
|
-
import { BRIEFING_TEMPLATE_ROOT, WRAPPER_BLOCK_BEGIN, WRAPPER_BLOCK_END,
|
|
18
|
+
import { BRIEFING_TEMPLATE_ROOT, WRAPPER_BLOCK_BEGIN, WRAPPER_BLOCK_END, briefingWrappers, } from "../briefing/index.js";
|
|
20
19
|
const SECTION_RULE = "═".repeat(59);
|
|
21
20
|
/**
|
|
22
21
|
* Report the pristine/edited/unclassifiable split for a conflict set.
|
|
@@ -332,32 +331,15 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
|
|
|
332
331
|
if (incomingIsForward && !opts.allowOverwrite && !migrateFrom) {
|
|
333
332
|
const preflightProfile = profileFromBundleName(incomingBundleName);
|
|
334
333
|
if (preflightProfile) {
|
|
335
|
-
|
|
336
|
-
const probe = await syncMethodManagedCopilotHooks({
|
|
337
|
-
targetRepo: opts.targetRepo,
|
|
338
|
-
allowOverwrite: false,
|
|
339
|
-
checkOnly: true,
|
|
340
|
-
});
|
|
341
|
-
if (probe.refused) {
|
|
342
|
-
failures.push(`.github/hooks/*.json has adopter-modified method entries (${probe.driftedRefused.join(", ")}). Re-run with --allow-overwrite to rewrite them.`);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
else {
|
|
334
|
+
{
|
|
346
335
|
const probe = await syncMethodManagedSettings({
|
|
347
336
|
targetRepo: opts.targetRepo,
|
|
348
|
-
profile: preflightProfile,
|
|
349
337
|
allowOverwrite: false,
|
|
350
338
|
checkOnly: true,
|
|
351
|
-
hadPreviousSync: prevLock !== null,
|
|
352
339
|
...(opts.agentHarness ? { agentHarness: opts.agentHarness } : {}),
|
|
353
340
|
});
|
|
354
341
|
if (probe.refused) {
|
|
355
|
-
|
|
356
|
-
if (probe.drifted.length > 0)
|
|
357
|
-
conflicts.push(`mcpServers: ${probe.drifted.join(", ")}`);
|
|
358
|
-
if (probe.hooksDrifted.length > 0)
|
|
359
|
-
conflicts.push(`hooks: ${probe.hooksDrifted.join(", ")}`);
|
|
360
|
-
failures.push(`.claude/settings.json has adopter-modified method entries (${conflicts.join("; ")}). Re-run with --allow-overwrite to rewrite them.`);
|
|
342
|
+
failures.push(`.claude/settings.json has adopter-modified method entries (hooks: ${probe.hooksDrifted.join(", ")}). Re-run with --allow-overwrite to rewrite them.`);
|
|
361
343
|
}
|
|
362
344
|
}
|
|
363
345
|
// The CI host wrapper and the Method Validation module (M-ADR 0219): the same check the
|
|
@@ -377,13 +359,18 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
|
|
|
377
359
|
// templates; pre-ADR-0148 baselines skip the briefing pass entirely.
|
|
378
360
|
const shipsBriefing = bundle.managedPaths.some((p) => p.startsWith(`${BRIEFING_TEMPLATE_ROOT}/`));
|
|
379
361
|
if (shipsBriefing) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
362
|
+
// Every wrapper the bundle ships a template for: CLAUDE.md and, from M-ADR 0220, the
|
|
363
|
+
// AGENTS.md pointer. An unmarked file is the adopter's and refuses.
|
|
364
|
+
for (const w of briefingWrappers(opts.agentHarness ?? DEFAULT_AGENT_HARNESS)) {
|
|
365
|
+
if (!bundle.managedPaths.includes(`${BRIEFING_TEMPLATE_ROOT}/${w.templateRel}`))
|
|
366
|
+
continue;
|
|
367
|
+
const preflightWrapper = await tryReadFile(join(opts.targetRepo, w.outputRel));
|
|
368
|
+
if (preflightWrapper !== null &&
|
|
369
|
+
preflightWrapper !== "" &&
|
|
370
|
+
!(preflightWrapper.includes(WRAPPER_BLOCK_BEGIN) &&
|
|
371
|
+
preflightWrapper.includes(WRAPPER_BLOCK_END))) {
|
|
372
|
+
failures.push(`${w.outputRel} has adopter edits outside the method-managed markers. Re-run with --allow-overwrite to replace the file.`);
|
|
373
|
+
}
|
|
387
374
|
}
|
|
388
375
|
}
|
|
389
376
|
}
|
|
@@ -508,58 +495,21 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
|
|
|
508
495
|
}
|
|
509
496
|
const repoKind = repoKindFromProfile(profile);
|
|
510
497
|
await writeRepoKind(opts.targetRepo, repoKind);
|
|
511
|
-
//
|
|
512
|
-
|
|
513
|
-
// hooks). github-copilot-cli writes .github/hooks/*.json instead;
|
|
514
|
-
// its MCP wiring lands separately under ADR 0150 Phase 3.
|
|
515
|
-
if (opts.agentHarness === "github-copilot-cli") {
|
|
516
|
-
const copilotResult = await syncMethodManagedCopilotHooks({
|
|
517
|
-
targetRepo: opts.targetRepo,
|
|
518
|
-
allowOverwrite: opts.allowOverwrite || migrateFrom !== null,
|
|
519
|
-
});
|
|
520
|
-
if (copilotResult.refused) {
|
|
521
|
-
stdout(fail(`.github/hooks/*.json has adopter-modified method entries (${copilotResult.driftedRefused.join(", ")}). Re-run with --allow-overwrite to rewrite them.`));
|
|
522
|
-
return { exitCode: 1 };
|
|
523
|
-
}
|
|
524
|
-
const written = copilotResult.files.filter((f) => f.wrote);
|
|
525
|
-
if (written.length > 0) {
|
|
526
|
-
stdout(info(`Refreshed ${written.map((f) => f.relPath).join(", ")}.`));
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
else {
|
|
498
|
+
// The harness wiring: .claude/settings.json, the one supported harness's hooks (M-ADR 0220 §4.6).
|
|
499
|
+
{
|
|
530
500
|
const settingsResult = await syncMethodManagedSettings({
|
|
531
501
|
targetRepo: opts.targetRepo,
|
|
532
|
-
profile,
|
|
533
502
|
allowOverwrite: opts.allowOverwrite || migrateFrom !== null,
|
|
534
|
-
// A lock is the only sound evidence the managed entries were seeded
|
|
535
|
-
// before, which is what makes a re-seed distinguishable from a first
|
|
536
|
-
// sync into an adopter's own settings file.
|
|
537
|
-
hadPreviousSync: prevLock !== null,
|
|
538
503
|
...(opts.agentHarness ? { agentHarness: opts.agentHarness } : {}),
|
|
539
504
|
});
|
|
540
505
|
if (settingsResult.refused) {
|
|
541
|
-
|
|
542
|
-
if (settingsResult.drifted.length > 0)
|
|
543
|
-
conflicts.push(`mcpServers: ${settingsResult.drifted.join(", ")}`);
|
|
544
|
-
if (settingsResult.hooksDrifted.length > 0)
|
|
545
|
-
conflicts.push(`hooks: ${settingsResult.hooksDrifted.join(", ")}`);
|
|
546
|
-
stdout(fail(`.claude/settings.json has adopter-modified method entries (${conflicts.join("; ")}). Re-run with --allow-overwrite to rewrite them.`));
|
|
506
|
+
stdout(fail(`.claude/settings.json has adopter-modified method entries (hooks: ${settingsResult.hooksDrifted.join(", ")}). Re-run with --allow-overwrite to rewrite them.`));
|
|
547
507
|
return { exitCode: 1 };
|
|
548
508
|
}
|
|
549
|
-
// Drift-class line of its own, ahead of the refresh summary: these
|
|
550
|
-
// entries were seeded by an earlier sync and had been removed since.
|
|
551
|
-
// Restoring them is not the news — that they went missing is, and the
|
|
552
|
-
// same ids reappearing here every sync means something is removing them
|
|
553
|
-
// repeatedly.
|
|
554
|
-
if (settingsResult.reseeded.length > 0) {
|
|
555
|
-
stdout(info(`${settingsResult.path}: RE-SEEDED ${settingsResult.reseeded.length} method-managed entr${settingsResult.reseeded.length === 1 ? "y" : "ies"} (${settingsResult.reseeded.join(", ")}) — present after a previous sync, absent now. Restored; if this recurs, something in this repo is removing them.`));
|
|
556
|
-
}
|
|
557
509
|
if (settingsResult.wroteFile) {
|
|
558
510
|
const labels = [];
|
|
559
|
-
if (settingsResult.
|
|
560
|
-
labels.push(`
|
|
561
|
-
if (settingsResult.updated.length > 0)
|
|
562
|
-
labels.push(`mcp updated: ${settingsResult.updated.join(", ")}`);
|
|
511
|
+
if (settingsResult.retiredServers.length > 0)
|
|
512
|
+
labels.push(`removed the method's retired MCP server entry: ${settingsResult.retiredServers.join(", ")} (M-ADR 0220)`);
|
|
563
513
|
if (settingsResult.hooksChangedEvents.length > 0)
|
|
564
514
|
labels.push(`hooks: ${settingsResult.hooksChangedEvents.join(", ")}`);
|
|
565
515
|
stdout(info(`Refreshed ${settingsResult.path} (${labels.join("; ") || "no changes"}).`));
|
|
@@ -1,145 +1,99 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `.claude/settings.json` merge pass during `sdd-cli sync`.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* `.claude/settings.json` merge pass during `sdd-cli sync`. The method's hooks (see
|
|
3
|
+
* src/lib/hooks-seed/) are upserted when the repo runs the claude-code harness; every adopter
|
|
4
|
+
* entry and every other top-level key is preserved.
|
|
5
5
|
*
|
|
6
|
-
* The
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* The method seeds no MCP server (M-ADR 0220 §4.5). Sync removes the `sdd-mcp` entry it seeded
|
|
7
|
+
* before, without --allow-overwrite, because an entry whose command is the method's own
|
|
8
|
+
* `sdd-cli mcp serve` is the method's output: the same rule by which sync retired its own
|
|
9
|
+
* SDD_REPO_PATH value. An adopter's other servers, and an `sdd-mcp` entry that runs anything
|
|
10
|
+
* else, are left alone.
|
|
9
11
|
*
|
|
10
|
-
* Conflict handling mirrors ADR 0114: if the adopter has
|
|
11
|
-
*
|
|
12
|
-
* `--allow-overwrite` is passed.
|
|
12
|
+
* Conflict handling mirrors ADR 0114: if the adopter has modified a method-owned hook, sync
|
|
13
|
+
* refuses unless `--allow-overwrite` is passed.
|
|
13
14
|
*/
|
|
14
15
|
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
15
16
|
import { dirname, join, resolve } from "node:path";
|
|
16
|
-
import {
|
|
17
|
-
import { buildMethodManagedHooks, detectHooksDrift, upsertMethodManagedHooks, } from "../hooks-seed/index.js";
|
|
17
|
+
import { buildMethodManagedHooks, detectHooksDrift, normaliseOwnHookRenders, upsertMethodManagedHooks, } from "../hooks-seed/index.js";
|
|
18
18
|
export const SETTINGS_REL_PATH = ".claude/settings.json";
|
|
19
|
+
/** The server id the method used to seed. */
|
|
20
|
+
export const RETIRED_SERVER_ID = "sdd-mcp";
|
|
21
|
+
/**
|
|
22
|
+
* Whether an `mcpServers` entry is one the method seeded: `sdd-cli mcp serve` and no other field,
|
|
23
|
+
* with no env or only an SDD_REPO_PATH that resolves to the repo itself (the value older syncs
|
|
24
|
+
* wrote for a platform repo). An SDD_REPO_PATH pointing anywhere else was the adopter's own
|
|
25
|
+
* configuration, as is any other change, and the entry stays.
|
|
26
|
+
*/
|
|
27
|
+
export function isOwnSddMcpEntry(entry, targetRepo) {
|
|
28
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
29
|
+
return false;
|
|
30
|
+
const e = entry;
|
|
31
|
+
if (Object.keys(e).some((k) => k !== "command" && k !== "args" && k !== "env"))
|
|
32
|
+
return false;
|
|
33
|
+
if (e["command"] !== "sdd-cli")
|
|
34
|
+
return false;
|
|
35
|
+
const args = e["args"];
|
|
36
|
+
if (!Array.isArray(args) || args.length !== 2 || args[0] !== "mcp" || args[1] !== "serve")
|
|
37
|
+
return false;
|
|
38
|
+
const env = e["env"];
|
|
39
|
+
if (env === undefined)
|
|
40
|
+
return true;
|
|
41
|
+
if (!env || typeof env !== "object" || Array.isArray(env))
|
|
42
|
+
return false;
|
|
43
|
+
const keys = Object.keys(env);
|
|
44
|
+
if (keys.length !== 1 || keys[0] !== "SDD_REPO_PATH")
|
|
45
|
+
return false;
|
|
46
|
+
const path = env["SDD_REPO_PATH"];
|
|
47
|
+
return typeof path === "string" && resolve(targetRepo, path) === resolve(targetRepo);
|
|
48
|
+
}
|
|
49
|
+
/** The settings with the method's own `sdd-mcp` entry removed, and which ids went. */
|
|
50
|
+
export function retireOwnMcpServers(settings, targetRepo) {
|
|
51
|
+
const servers = settings["mcpServers"];
|
|
52
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers))
|
|
53
|
+
return { settings, retired: [] };
|
|
54
|
+
const current = servers;
|
|
55
|
+
if (!(RETIRED_SERVER_ID in current) || !isOwnSddMcpEntry(current[RETIRED_SERVER_ID], targetRepo)) {
|
|
56
|
+
return { settings, retired: [] };
|
|
57
|
+
}
|
|
58
|
+
const { [RETIRED_SERVER_ID]: _retired, ...rest } = current;
|
|
59
|
+
const { mcpServers: _servers, ...others } = settings;
|
|
60
|
+
return {
|
|
61
|
+
settings: Object.keys(rest).length > 0 ? { ...others, mcpServers: rest } : others,
|
|
62
|
+
retired: [RETIRED_SERVER_ID],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
19
65
|
export async function syncMethodManagedSettings(opts) {
|
|
20
66
|
const absPath = join(opts.targetRepo, SETTINGS_REL_PATH);
|
|
67
|
+
const fileExisted = await fileExists(absPath);
|
|
21
68
|
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
69
|
const hooksActive = opts.agentHarness === "claude-code";
|
|
54
70
|
const ownedHooks = hooksActive ? buildMethodManagedHooks() : undefined;
|
|
55
71
|
const hooksDrifted = hooksActive && ownedHooks
|
|
56
|
-
? detectHooksDrift(existing, ownedHooks)
|
|
72
|
+
? detectHooksDrift(normaliseOwnHookRenders(existing, ownedHooks), ownedHooks)
|
|
57
73
|
: [];
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
+
const base = { path: SETTINGS_REL_PATH, hooksDrifted };
|
|
75
|
+
if (hooksDrifted.length > 0 && !opts.allowOverwrite) {
|
|
76
|
+
return { ...base, retiredServers: [], hooksChangedEvents: [], wroteFile: false, refused: true };
|
|
74
77
|
}
|
|
78
|
+
const retirement = retireOwnMcpServers(existing, opts.targetRepo);
|
|
75
79
|
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
|
-
};
|
|
80
|
+
return { ...base, retiredServers: retirement.retired, hooksChangedEvents: [], wroteFile: false, refused: false };
|
|
88
81
|
}
|
|
89
|
-
|
|
90
|
-
let nextSettings = mcpResult.settings;
|
|
82
|
+
let nextSettings = retirement.settings;
|
|
91
83
|
let hooksChangedEvents = [];
|
|
92
84
|
if (hooksActive && ownedHooks) {
|
|
93
85
|
const hooksResult = upsertMethodManagedHooks(nextSettings, ownedHooks);
|
|
94
86
|
nextSettings = hooksResult.settings;
|
|
95
87
|
hooksChangedEvents = [...hooksResult.changedEvents];
|
|
96
88
|
}
|
|
97
|
-
const changed =
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
};
|
|
89
|
+
const changed = retirement.retired.length > 0 || hooksChangedEvents.length > 0;
|
|
90
|
+
// Nothing to write: leave the file as it is, and never create one just to hold nothing.
|
|
91
|
+
if (!changed || (!fileExisted && Object.keys(nextSettings).length === 0)) {
|
|
92
|
+
return { ...base, retiredServers: [], hooksChangedEvents: [], wroteFile: false, refused: false };
|
|
111
93
|
}
|
|
112
94
|
await mkdir(dirname(absPath), { recursive: true });
|
|
113
95
|
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();
|
|
96
|
+
return { ...base, retiredServers: retirement.retired, hooksChangedEvents, wroteFile: true, refused: false };
|
|
143
97
|
}
|
|
144
98
|
async function readSettings(absPath) {
|
|
145
99
|
if (!(await fileExists(absPath)))
|
|
@@ -167,50 +121,4 @@ async function fileExists(path) {
|
|
|
167
121
|
return false;
|
|
168
122
|
}
|
|
169
123
|
}
|
|
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
124
|
//# sourceMappingURL=settings-merge.js.map
|
package/dist/maintain-program.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// (`context pull`). Plus the whole verify surface, because the personas nest.
|
|
11
11
|
//
|
|
12
12
|
// ⚠ As with sdd-verify, the boundary is ABSENCE, not a menu: the package ships only the import
|
|
13
|
-
// closure of maintain-bin.js, so authoring verbs (init, scaffold, plan,
|
|
13
|
+
// closure of maintain-bin.js, so authoring verbs (init, scaffold, plan, conformance,
|
|
14
14
|
// contracts, api-surface, snapshot, metamodel, migration-map) are not present to be reached.
|
|
15
15
|
// That is why every registration below imports a per-subcommand registrar, never a barrel.
|
|
16
16
|
// Unclassified verbs default to AUTHOR (M-ADR 0218 §4.2) and stay out until classified.
|
|
@@ -18,6 +18,7 @@ import { Command, CommanderError } from "commander";
|
|
|
18
18
|
import { getPackageVersion } from "./lib/package-version.js";
|
|
19
19
|
import { registerVerifySurface, rejectOutsideSurface, VERIFY_SURFACE } from "./verify-program.js";
|
|
20
20
|
import { registerCatalogueAggregate, registerCatalogueBuild, registerCatalogueDiff, registerCatalogueGraph } from "./verbs/catalogue-maintain.js";
|
|
21
|
+
import { registerCatalogueCheck } from "./verbs/catalogue-check.js";
|
|
21
22
|
import { registerContext } from "./verbs/context.js";
|
|
22
23
|
import { registerGenerateBriefing } from "./verbs/generate-briefing.js";
|
|
23
24
|
import { registerPackBuild } from "./verbs/pack-build.js";
|
|
@@ -30,6 +31,7 @@ export const MAINTAIN_ONLY_SURFACE = [
|
|
|
30
31
|
"generate-briefing",
|
|
31
32
|
"context pull",
|
|
32
33
|
"catalogue build",
|
|
34
|
+
"catalogue check",
|
|
33
35
|
"catalogue aggregate",
|
|
34
36
|
"catalogue graph",
|
|
35
37
|
"catalogue diff",
|
|
@@ -65,7 +67,7 @@ export function buildMaintainProgram() {
|
|
|
65
67
|
.name("sdd-maintain")
|
|
66
68
|
.description("Bring an SDD repository current and rebuild its derived model — sync, briefing, catalogue build and aggregate, pack build, context pull — plus every read-only verify verb. Cannot author specification content.")
|
|
67
69
|
.version(getPackageVersion(), "-v, --version")
|
|
68
|
-
.addHelpText("after", `\nSpec: ${SPEC_URL}\nAuthoring verbs (
|
|
70
|
+
.addHelpText("after", `\nSpec: ${SPEC_URL}\nAuthoring verbs (scaffold, plan, conformance, …) are in @sdd-method/author-cli; init is in @sdd-method/sdd-cli.`)
|
|
69
71
|
.exitOverride()
|
|
70
72
|
.configureOutput({
|
|
71
73
|
writeOut: (str) => process.stdout.write(str),
|
|
@@ -80,6 +82,7 @@ export function buildMaintainProgram() {
|
|
|
80
82
|
const catalogue = program.commands.find((c) => c.name() === "catalogue");
|
|
81
83
|
catalogue.description("Build, aggregate, validate and query per-SDD catalogues (ADR 0134) — the derived model");
|
|
82
84
|
registerCatalogueBuild(catalogue);
|
|
85
|
+
registerCatalogueCheck(catalogue);
|
|
83
86
|
registerCatalogueAggregate(catalogue);
|
|
84
87
|
registerCatalogueGraph(catalogue);
|
|
85
88
|
registerCatalogueDiff(catalogue);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { checkCataloguesFresh } from "../lib/catalogue/freshness.js";
|
|
3
|
+
/**
|
|
4
|
+
* `catalogue check` (M-ADR 0220 §4.2, ruling 3): whether the committed catalogues are what the
|
|
5
|
+
* committed specifications build to. It builds into temporary space and writes nothing to the
|
|
6
|
+
* repository: `catalogue build --check` under its own name, so the author's surface holds no verb
|
|
7
|
+
* that writes the model. Its own registrar, so an artefact can carry it without the build,
|
|
8
|
+
* aggregate and graph registrations in catalogue-maintain.ts.
|
|
9
|
+
*/
|
|
10
|
+
export function registerCatalogueCheck(catalogue) {
|
|
11
|
+
catalogue
|
|
12
|
+
.command("check")
|
|
13
|
+
.description("Report whether the committed catalogues/ are what the committed specifications build to; builds in a temporary directory and writes nothing (exit 1 lists every stale file)")
|
|
14
|
+
.option("--sdd-path <path>", "Path to the SDD repo root (defaults to cwd)")
|
|
15
|
+
.option("--strict", "Treat build findings as failures")
|
|
16
|
+
.option("--snapshot-store <dir>", "Snapshot store root the binding's snapshot_area is relative to (default: $SDD_SNAPSHOT_STORE; M-ADR 0214)")
|
|
17
|
+
.action(async (opts) => {
|
|
18
|
+
const snapshotStore = opts.snapshotStore ?? process.env.SDD_SNAPSHOT_STORE;
|
|
19
|
+
const r = await checkCataloguesFresh(resolve(opts.sddPath ?? process.cwd()), {
|
|
20
|
+
label: "catalogue check",
|
|
21
|
+
...(opts.strict !== undefined ? { strict: opts.strict } : {}),
|
|
22
|
+
...(snapshotStore !== undefined ? { snapshotStore } : {}),
|
|
23
|
+
}, (s) => process.stdout.write(s));
|
|
24
|
+
if (r.exitCode !== 0)
|
|
25
|
+
process.exit(r.exitCode);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=catalogue-check.js.map
|
package/dist/verbs/context.js
CHANGED
|
@@ -8,7 +8,7 @@ export function registerContext(program) {
|
|
|
8
8
|
.description("Briefing for a code repository an SDD specifies (M-ADR 0128 §1, M-ADR 0150 §4.4)");
|
|
9
9
|
context
|
|
10
10
|
.command("pull")
|
|
11
|
-
.description("Write agent-instructions.md
|
|
11
|
+
.description("Write agent-instructions.md and the harness wrapper's method-managed block into this code repository, rendered from the templates its upstream SDD carries. The SDD is found by looking this repository up in the catalogues/repositories.csv of the SDDs beside it. Writes nothing else.")
|
|
12
12
|
.option("--target-repo <dir>", "the code repository to write into", resolvePathOption, process.cwd())
|
|
13
13
|
.option("--sdd <dir>", "the upstream SDD, skipping discovery", resolvePathOption)
|
|
14
14
|
.option("--repo <id>", "this repository's id in the SDD's catalogue (default: its git top-level directory name)")
|
package/dist/verify-program.js
CHANGED
|
@@ -91,7 +91,7 @@ export function buildVerifyProgram() {
|
|
|
91
91
|
.name("sdd-verify")
|
|
92
92
|
.description("Verify an SDD repository without changing it — validate, list, and read-only catalogue and pack checks. Cannot write to the repository it is pointed at.")
|
|
93
93
|
.version(getPackageVersion(), "-v, --version")
|
|
94
|
-
.addHelpText("after", `\nSpec: ${SPEC_URL}\nAuthoring and maintenance verbs (sync, catalogue build, init, scaffold,
|
|
94
|
+
.addHelpText("after", `\nSpec: ${SPEC_URL}\nAuthoring and maintenance verbs (sync, catalogue build, init, scaffold, …) are in @sdd-method/sdd-cli.`)
|
|
95
95
|
.exitOverride()
|
|
96
96
|
.configureOutput({
|
|
97
97
|
writeOut: (str) => process.stdout.write(str),
|
package/dist/verify-version.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdd-method/maintain-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"sddCliVersion": "1.
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"sddCliVersion": "1.43.0",
|
|
5
5
|
"description": "Bring an SDD repository current and rebuild its derived model — sync, briefing, catalogue build and aggregate, pack build, context pull — plus every read-only verify verb. Ships only the code those verbs reach; it cannot author specification content. Authoring is in @sdd-method/sdd-cli.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"type": "module",
|