@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,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CI host bindings (M-ADR 0219 §4.4): `sync --ci-binding github | gitlab | jenkins` renders ONE host
|
|
3
|
+
* wrapper, which installs the pinned Dagger CLI and calls the Method Validation module, and writes
|
|
4
|
+
* the module itself at MODULE_DIR (§4.3). The wrapper is a method-managed block inside the host's
|
|
5
|
+
* file, so adopter content outside the markers survives; the module is wholly method-managed.
|
|
6
|
+
*
|
|
7
|
+
* Both are digest-tracked in the lock (M-ADR 0218 §4.7): content that still hashes to what sync last
|
|
8
|
+
* wrote is pristine and re-renders with no override, and only an edited copy needs --allow-overwrite.
|
|
9
|
+
* The binding is remembered in the lock too (`ci_binding:`, absent meaning github), so an estate
|
|
10
|
+
* chooses it once.
|
|
11
|
+
*/
|
|
12
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
import { BLOCK_BEGIN, BLOCK_END, WORKFLOW_REL_PATH, renderMethodManagedWorkflowBlock, renderMethodValidationWorkflow, } from "../init/ci-workflow.js";
|
|
16
|
+
import { detectBlockDrift, writeMarkedBlockFile } from "../sync/yaml-block-merge.js";
|
|
17
|
+
import { DAGGER_VERSION, MODULE_CALL, MODULE_DIR, moduleSha256, renderModuleFiles } from "./module.js";
|
|
18
|
+
export const CI_BINDINGS = ["github", "gitlab", "jenkins"];
|
|
19
|
+
/** M-ADR 0219 §8.2 (ruled 2026-10-16): what every SDD has before it chooses. */
|
|
20
|
+
export const DEFAULT_CI_BINDING = "github";
|
|
21
|
+
/**
|
|
22
|
+
* Runner labels (sync --ci-runner): where the host runs the job. One label is a word of letters,
|
|
23
|
+
* digits, '.', '_' or '-', so a label can never inject YAML or Groovy into a wrapper.
|
|
24
|
+
*/
|
|
25
|
+
export const CI_RUNNER_DEFAULT = "default";
|
|
26
|
+
const RUNNER_LABEL = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
27
|
+
export function parseCiRunner(s) {
|
|
28
|
+
const labels = s.split(",").map((l) => l.trim());
|
|
29
|
+
return labels.length > 0 && labels.every((l) => RUNNER_LABEL.test(l)) ? labels : null;
|
|
30
|
+
}
|
|
31
|
+
export function isCiBinding(s) {
|
|
32
|
+
return CI_BINDINGS.includes(s);
|
|
33
|
+
}
|
|
34
|
+
const HASH = { begin: BLOCK_BEGIN, end: BLOCK_END };
|
|
35
|
+
// Groovy has no '#' comments.
|
|
36
|
+
const SLASH = { begin: "// method-managed: begin", end: "// method-managed: end" };
|
|
37
|
+
function header(comment, profile) {
|
|
38
|
+
return [
|
|
39
|
+
`${comment} Method Validation per M-ADR 0129, run by the method's Dagger module (M-ADR 0219).`,
|
|
40
|
+
`${comment} Refreshed by \`sdd-cli sync\`; content inside the method-managed`,
|
|
41
|
+
`${comment} markers must not be hand-edited (sync refuses without --allow-overwrite).`,
|
|
42
|
+
`${comment} Profile: ${profile}`,
|
|
43
|
+
comment,
|
|
44
|
+
`${comment} The job is defined once, in ${MODULE_DIR}/, and this file only calls it,`,
|
|
45
|
+
`${comment} so CI on any host and a local run cannot disagree. The runner must`,
|
|
46
|
+
`${comment} provide a container engine Dagger can drive (M-ADR 0219 §4.5).`,
|
|
47
|
+
comment,
|
|
48
|
+
`${comment} Run it locally:`,
|
|
49
|
+
`${comment} ${MODULE_CALL}`,
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
// GitLab: a Docker-in-Docker service, the shape GitLab documents for Dagger. The runner must allow
|
|
53
|
+
// privileged containers; that is the adopter's infrastructure (M-ADR 0219 §4.5). No `stage:`, so
|
|
54
|
+
// the job lands in the default `test` stage whatever stages the adopter declares around it.
|
|
55
|
+
function gitlabBlock(profile, runner) {
|
|
56
|
+
return [
|
|
57
|
+
HASH.begin,
|
|
58
|
+
...header("#", profile),
|
|
59
|
+
"",
|
|
60
|
+
"method-validation:",
|
|
61
|
+
" image: docker:27.3.1",
|
|
62
|
+
...(runner !== undefined && runner.length > 0 ? [` tags: [${runner.join(", ")}]`] : []),
|
|
63
|
+
" services:",
|
|
64
|
+
" - docker:27.3.1-dind",
|
|
65
|
+
" variables:",
|
|
66
|
+
" DOCKER_HOST: tcp://docker:2376",
|
|
67
|
+
' DOCKER_TLS_CERTDIR: "/certs"',
|
|
68
|
+
' DOCKER_TLS_VERIFY: "1"',
|
|
69
|
+
' DOCKER_CERT_PATH: "/certs/client"',
|
|
70
|
+
" rules:",
|
|
71
|
+
' - if: $CI_PIPELINE_SOURCE == "merge_request_event"',
|
|
72
|
+
" - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH",
|
|
73
|
+
" before_script:",
|
|
74
|
+
" - apk add --no-cache curl",
|
|
75
|
+
` - curl -fsSL https://dl.dagger.io/dagger/install.sh | BIN_DIR=/usr/local/bin DAGGER_VERSION=${DAGGER_VERSION} sh`,
|
|
76
|
+
" script:",
|
|
77
|
+
` - ${MODULE_CALL}`,
|
|
78
|
+
HASH.end,
|
|
79
|
+
].join("\n");
|
|
80
|
+
}
|
|
81
|
+
// Jenkins: a declarative pipeline at its own path, so it never collides with an adopter's
|
|
82
|
+
// Jenkinsfile; the job's Script Path points at it. The agent must reach a Docker daemon. Dagger is
|
|
83
|
+
// installed outside the workspace, so the binary is not uploaded with --source.
|
|
84
|
+
function jenkinsBlock(profile, runner) {
|
|
85
|
+
return [
|
|
86
|
+
SLASH.begin,
|
|
87
|
+
...header("//", profile),
|
|
88
|
+
"",
|
|
89
|
+
"pipeline {",
|
|
90
|
+
runner !== undefined && runner.length > 0 ? ` agent { label '${runner.join(" && ")}' }` : " agent any",
|
|
91
|
+
" stages {",
|
|
92
|
+
" stage('Method Validation') {",
|
|
93
|
+
" steps {",
|
|
94
|
+
` sh 'curl -fsSL https://dl.dagger.io/dagger/install.sh | BIN_DIR="$WORKSPACE@tmp/dagger" DAGGER_VERSION=${DAGGER_VERSION} sh'`,
|
|
95
|
+
` sh 'PATH="$WORKSPACE@tmp/dagger:$PATH" ${MODULE_CALL}'`,
|
|
96
|
+
" }",
|
|
97
|
+
" }",
|
|
98
|
+
" }",
|
|
99
|
+
"}",
|
|
100
|
+
SLASH.end,
|
|
101
|
+
].join("\n");
|
|
102
|
+
}
|
|
103
|
+
export const HOST_WRAPPERS = {
|
|
104
|
+
github: {
|
|
105
|
+
relPath: WORKFLOW_REL_PATH,
|
|
106
|
+
markers: HASH,
|
|
107
|
+
block: renderMethodManagedWorkflowBlock,
|
|
108
|
+
file: renderMethodValidationWorkflow,
|
|
109
|
+
},
|
|
110
|
+
gitlab: {
|
|
111
|
+
relPath: ".gitlab-ci.yml",
|
|
112
|
+
markers: HASH,
|
|
113
|
+
block: gitlabBlock,
|
|
114
|
+
file: (p, r) => gitlabBlock(p, r) + "\n\n# Adopter-specific jobs go below, outside the method-managed markers.\n",
|
|
115
|
+
},
|
|
116
|
+
jenkins: {
|
|
117
|
+
relPath: ".jenkins/method-validation.Jenkinsfile",
|
|
118
|
+
markers: SLASH,
|
|
119
|
+
block: jenkinsBlock,
|
|
120
|
+
file: (p, r) => jenkinsBlock(p, r) + "\n",
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
/** sha256 of a host file's method-managed block (begin marker through end marker), or null without one. */
|
|
124
|
+
export function blockSha256(text, markers) {
|
|
125
|
+
if (text === null)
|
|
126
|
+
return null;
|
|
127
|
+
const b = text.indexOf(markers.begin);
|
|
128
|
+
const e = b === -1 ? -1 : text.indexOf(markers.end, b);
|
|
129
|
+
if (b === -1 || e === -1)
|
|
130
|
+
return null;
|
|
131
|
+
return createHash("sha256").update(text.slice(b, e + markers.end.length)).digest("hex");
|
|
132
|
+
}
|
|
133
|
+
/** Bring the host wrapper and the module up to this sdd-cli's render, or say why not. */
|
|
134
|
+
export async function syncCiBinding(o) {
|
|
135
|
+
const failures = [];
|
|
136
|
+
const log = [];
|
|
137
|
+
const host = HOST_WRAPPERS[o.binding];
|
|
138
|
+
const sameBinding = (o.prevLock?.ciBinding ?? DEFAULT_CI_BINDING) === o.binding;
|
|
139
|
+
// 1. The host wrapper.
|
|
140
|
+
const wrapperAbs = join(o.targetRepo, host.relPath);
|
|
141
|
+
const existing = await tryRead(wrapperAbs);
|
|
142
|
+
const block = host.block(o.profile, o.runner);
|
|
143
|
+
const drifted = existing !== null && detectBlockDrift(existing, block, host.markers);
|
|
144
|
+
const recorded = sameBinding ? o.prevLock?.ciWorkflowBlockSha256 : undefined;
|
|
145
|
+
const pristine = existing !== null && recorded !== undefined && blockSha256(existing, host.markers) === recorded;
|
|
146
|
+
const markerless = existing !== null && existing !== "" && !(existing.includes(host.markers.begin) && existing.includes(host.markers.end));
|
|
147
|
+
if (drifted && pristine) {
|
|
148
|
+
log.push(`${host.relPath}: method-managed block unmodified since the last sync (digest matches the lock); re-rendering it for this sdd-cli.`);
|
|
149
|
+
}
|
|
150
|
+
if (drifted && !pristine && !o.allowDrift) {
|
|
151
|
+
failures.push(`${host.relPath}: the method-managed block differs from what this sdd-cli renders. Either the generator changed since the last sync (expected after a CLI upgrade) or the block was hand-edited. Re-run with --allow-overwrite to re-render it; adopter content outside the markers is preserved either way.`);
|
|
152
|
+
}
|
|
153
|
+
else if (markerless && !o.allowOverwrite) {
|
|
154
|
+
failures.push(`${host.relPath} exists without method-managed markers — refusing to overwrite adopter content. Re-run with --allow-overwrite to replace the file.`);
|
|
155
|
+
}
|
|
156
|
+
// 2. The module: every file it renders, compared as one set.
|
|
157
|
+
const rendered = renderModuleFiles();
|
|
158
|
+
const onDisk = [];
|
|
159
|
+
for (const [rel] of rendered)
|
|
160
|
+
onDisk.push([rel, await tryRead(join(o.targetRepo, MODULE_DIR, rel))]);
|
|
161
|
+
const present = onDisk.some(([, c]) => c !== null);
|
|
162
|
+
const matches = rendered.every(([rel, c], i) => onDisk[i][0] === rel && onDisk[i][1] === c);
|
|
163
|
+
const modulePristine = o.prevLock?.ciModuleSha256 !== undefined && moduleSha256(onDisk) === o.prevLock.ciModuleSha256;
|
|
164
|
+
if (present && !matches && modulePristine) {
|
|
165
|
+
log.push(`${MODULE_DIR}/: unmodified since the last sync (digest matches the lock); re-rendering it for this sdd-cli.`);
|
|
166
|
+
}
|
|
167
|
+
if (present && !matches && !modulePristine && !o.allowDrift) {
|
|
168
|
+
failures.push(`${MODULE_DIR}/: the method-managed module differs from what this sdd-cli renders. Either the generator changed since the last sync (expected after a CLI upgrade) or a file was hand-edited. Re-run with --allow-overwrite to re-render it.`);
|
|
169
|
+
}
|
|
170
|
+
if (o.checkOnly || failures.length > 0) {
|
|
171
|
+
return { failures, log, wrapperSha: null, moduleSha: moduleSha256(onDisk) };
|
|
172
|
+
}
|
|
173
|
+
const written = await writeMarkedBlockFile(wrapperAbs, existing === null ? host.file(o.profile, o.runner) : block, host.markers, o.allowOverwrite);
|
|
174
|
+
if (written.changed)
|
|
175
|
+
log.push(`${existing === null ? "Wrote" : "Refreshed"} ${host.relPath}.`);
|
|
176
|
+
if (!matches) {
|
|
177
|
+
for (const [rel, content] of rendered) {
|
|
178
|
+
const abs = join(o.targetRepo, MODULE_DIR, rel);
|
|
179
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
180
|
+
await writeFile(abs, content, "utf8");
|
|
181
|
+
}
|
|
182
|
+
log.push(`${present ? "Refreshed" : "Wrote"} ${MODULE_DIR}/ (the Method Validation module, M-ADR 0219).`);
|
|
183
|
+
}
|
|
184
|
+
// A binding switch leaves the previous host's file alone: it is the adopter's to delete once the
|
|
185
|
+
// new host runs, and deleting it here would remove a job some branch protection may still require.
|
|
186
|
+
if (!sameBinding) {
|
|
187
|
+
const was = o.prevLock?.ciBinding;
|
|
188
|
+
const previous = HOST_WRAPPERS[was !== undefined && isCiBinding(was) ? was : DEFAULT_CI_BINDING];
|
|
189
|
+
if ((await tryRead(join(o.targetRepo, previous.relPath))) !== null) {
|
|
190
|
+
log.push(`CI binding is now ${o.binding}; ${previous.relPath} (the previous binding) is left in place. Delete it once ${host.relPath} runs.`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
failures,
|
|
195
|
+
log,
|
|
196
|
+
wrapperSha: blockSha256(await tryRead(wrapperAbs), host.markers),
|
|
197
|
+
moduleSha: moduleSha256(rendered),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
async function tryRead(path) {
|
|
201
|
+
try {
|
|
202
|
+
await access(path);
|
|
203
|
+
return await readFile(path, "utf8");
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** Write the module into a new SDD (init); returns the paths written, relative to the SDD root. */
|
|
210
|
+
export async function writeMethodValidationModule(targetRepo) {
|
|
211
|
+
const written = [];
|
|
212
|
+
for (const [rel, content] of renderModuleFiles()) {
|
|
213
|
+
const abs = join(targetRepo, MODULE_DIR, rel);
|
|
214
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
215
|
+
await writeFile(abs, content, "utf8");
|
|
216
|
+
written.push(`${MODULE_DIR}/${rel}`);
|
|
217
|
+
}
|
|
218
|
+
return written;
|
|
219
|
+
}
|
|
220
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Method Validation Dagger module (M-ADR 0219 §4.1–§4.3): the ONLY definition of the
|
|
3
|
+
* method-managed validation job. `sync` writes these files into every SDD at MODULE_DIR, with the
|
|
4
|
+
* exact verify-cli and maintain-cli versions released alongside this sdd-cli substituted in, and
|
|
5
|
+
* every CI host's wrapper (./index.ts) just calls the module.
|
|
6
|
+
*
|
|
7
|
+
* The module's source lives here, as text, rather than as files beside the package: sync runs from
|
|
8
|
+
* three packages (sdd-cli, maintain-cli, and the persona builds' import closure), and a string
|
|
9
|
+
* travels with whichever one imports it, where an asset directory would have to be copied into each.
|
|
10
|
+
* `scripts/dagger-parity.sh` renders it from the built package, so the parity harness runs exactly
|
|
11
|
+
* what sync writes.
|
|
12
|
+
*/
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { getMaintainCliVersion, getVerifyCliVersion } from "../package-version.js";
|
|
15
|
+
/** Where the module lives in an SDD (M-ADR 0219 §4.3, ruled 2026-10-16). */
|
|
16
|
+
export const MODULE_DIR = ".dagger/method-validation";
|
|
17
|
+
/**
|
|
18
|
+
* The Dagger engine the module declares and every wrapper installs, pinned to one value so the CLI a
|
|
19
|
+
* wrapper installs and the engine the module asks for can never disagree (M-ADR 0219 §6). An engine
|
|
20
|
+
* upgrade is a release concern: change it here, run scripts/dagger-parity.sh, release.
|
|
21
|
+
*/
|
|
22
|
+
export const DAGGER_VERSION = "0.21.10";
|
|
23
|
+
/** The command every host wrapper runs. */
|
|
24
|
+
export const MODULE_CALL = `dagger call -m ${MODULE_DIR} method-validation --source .`;
|
|
25
|
+
const VERIFY_PIN = "@@VERIFY_CLI_VERSION@@";
|
|
26
|
+
// The module's SDK import, substituted at render: written literally, the persona builds' import-closure
|
|
27
|
+
// scan would read this text as an import of the CLI's own and refuse a dependency it does not declare.
|
|
28
|
+
const DAGGER_SDK = "@@DAGGER_SDK@@";
|
|
29
|
+
const MAINTAIN_PIN = "@@MAINTAIN_CLI_VERSION@@";
|
|
30
|
+
// The module's own source. String.raw keeps it byte-for-byte, so it holds no backtick; its only
|
|
31
|
+
// substitutions are the three markers above, which renderModuleFiles replaces.
|
|
32
|
+
const INDEX_TS = String.raw `/**
|
|
33
|
+
* Method Validation (M-ADR 0219): the method-managed validation job, as one Dagger module. Every CI
|
|
34
|
+
* host calls it, and none keeps a step list of its own. Written by sdd-cli sync and method-managed:
|
|
35
|
+
* sync refuses to overwrite an edited copy without --allow-overwrite.
|
|
36
|
+
*
|
|
37
|
+
* Run it anywhere a container engine runs:
|
|
38
|
+
* dagger call -m .dagger/method-validation method-validation --source .
|
|
39
|
+
*
|
|
40
|
+
* The steps are the method's job plan, read at run time from "sdd-verify validate --list". Every
|
|
41
|
+
* step runs even after an earlier one fails, so one red step never hides the verdicts after it. The
|
|
42
|
+
* job fails only when a non-advisory step fails; the catalogue freshness step is advisory.
|
|
43
|
+
*/
|
|
44
|
+
import { dag, Container, Directory, File, object, func, ReturnType } from ${DAGGER_SDK}
|
|
45
|
+
|
|
46
|
+
// Pinned, so a run is reproducible and every tool the validators shell out to is named here.
|
|
47
|
+
const NODE_IMAGE = "node:20-bookworm"
|
|
48
|
+
const YQ_VERSION = "v4.44.3"
|
|
49
|
+
|
|
50
|
+
interface PlanStep {
|
|
51
|
+
id: string
|
|
52
|
+
title: string
|
|
53
|
+
tool: "sdd-verify" | "sdd-maintain" | "bash"
|
|
54
|
+
args: string[]
|
|
55
|
+
advisory: boolean
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@object()
|
|
59
|
+
export class MethodValidation {
|
|
60
|
+
/**
|
|
61
|
+
* The toolchain: Node, git, bash, python3, ripgrep, a pinned yq, and the exact-pinned verify and
|
|
62
|
+
* maintain CLIs. A tarball, when given, replaces the published package (a release candidate).
|
|
63
|
+
*/
|
|
64
|
+
@func()
|
|
65
|
+
toolchain(
|
|
66
|
+
verifyVersion: string = "${VERIFY_PIN}",
|
|
67
|
+
maintainVersion: string = "${MAINTAIN_PIN}",
|
|
68
|
+
verifyTarball?: File,
|
|
69
|
+
maintainTarball?: File,
|
|
70
|
+
): Container {
|
|
71
|
+
let c = dag
|
|
72
|
+
.container()
|
|
73
|
+
.from(NODE_IMAGE)
|
|
74
|
+
.withExec(["sh", "-c", "apt-get update -qq && apt-get install -y -qq ripgrep python3 >/dev/null"])
|
|
75
|
+
.withExec([
|
|
76
|
+
"sh",
|
|
77
|
+
"-c",
|
|
78
|
+
"curl -fsSL -o /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/" +
|
|
79
|
+
YQ_VERSION +
|
|
80
|
+
"/yq_linux_$(dpkg --print-architecture) && chmod +x /usr/local/bin/yq",
|
|
81
|
+
])
|
|
82
|
+
const pkgs: string[] = []
|
|
83
|
+
if (verifyTarball) {
|
|
84
|
+
c = c.withFile("/pkgs/verify-cli.tgz", verifyTarball)
|
|
85
|
+
pkgs.push("/pkgs/verify-cli.tgz")
|
|
86
|
+
} else pkgs.push("@sdd-method/verify-cli@" + verifyVersion)
|
|
87
|
+
if (maintainTarball) {
|
|
88
|
+
c = c.withFile("/pkgs/maintain-cli.tgz", maintainTarball)
|
|
89
|
+
pkgs.push("/pkgs/maintain-cli.tgz")
|
|
90
|
+
} else pkgs.push("@sdd-method/maintain-cli@" + maintainVersion)
|
|
91
|
+
return c.withExec(["npm", "install", "-g", "--silent", ...pkgs])
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Run Method Validation on an SDD repository. Returns a step-by-step report, and throws (so the
|
|
96
|
+
* CI host's job fails) when any non-advisory step failed.
|
|
97
|
+
*/
|
|
98
|
+
@func()
|
|
99
|
+
async methodValidation(
|
|
100
|
+
source: Directory,
|
|
101
|
+
verifyVersion: string = "${VERIFY_PIN}",
|
|
102
|
+
maintainVersion: string = "${MAINTAIN_PIN}",
|
|
103
|
+
verifyTarball?: File,
|
|
104
|
+
maintainTarball?: File,
|
|
105
|
+
): Promise<string> {
|
|
106
|
+
const base = this.toolchain(verifyVersion, maintainVersion, verifyTarball, maintainTarball)
|
|
107
|
+
.withMountedDirectory("/src", source)
|
|
108
|
+
.withWorkdir("/src")
|
|
109
|
+
const versions = (await base.withExec(["sh", "-c", "sdd-verify --version; sdd-maintain --version"]).stdout()).split("\n")
|
|
110
|
+
const plan = JSON.parse(await base.withExec(["sdd-verify", "validate", "--list"]).stdout()) as {
|
|
111
|
+
profile: string
|
|
112
|
+
steps: PlanStep[]
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const lines = ["Method Validation: profile " + plan.profile + "; sdd-verify " + versions[0] + ", sdd-maintain " + versions[1]]
|
|
116
|
+
let failed = 0
|
|
117
|
+
for (const step of plan.steps) {
|
|
118
|
+
const ran = base.withExec([step.tool, ...step.args], { expect: ReturnType.Any })
|
|
119
|
+
const code = await ran.exitCode()
|
|
120
|
+
const verdict = code === 0 ? "PASS" : step.advisory ? "ADVISORY" : "FAIL"
|
|
121
|
+
if (code !== 0 && !step.advisory) failed++
|
|
122
|
+
lines.push(verdict.padEnd(8) + " exit " + code + " " + step.id + " (" + step.title + ")")
|
|
123
|
+
if (code !== 0) {
|
|
124
|
+
const out = (await ran.stdout()) + (await ran.stderr())
|
|
125
|
+
lines.push(...out.trim().split("\n").slice(-6).map((l) => " " + l))
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
lines.push(failed === 0 ? "RESULT: PASS" : "RESULT: FAIL (" + failed + " failing step(s))")
|
|
129
|
+
const report = lines.join("\n")
|
|
130
|
+
if (failed > 0) throw new Error(report)
|
|
131
|
+
return report
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
`;
|
|
135
|
+
const DAGGER_JSON = `{
|
|
136
|
+
"name": "method-validation",
|
|
137
|
+
"engineVersion": "v${DAGGER_VERSION}",
|
|
138
|
+
"sdk": {
|
|
139
|
+
"source": "typescript"
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
`;
|
|
143
|
+
const PACKAGE_JSON = `{
|
|
144
|
+
"type": "module",
|
|
145
|
+
"dependencies": {
|
|
146
|
+
"typescript": "5.9.3"
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
`;
|
|
150
|
+
const TSCONFIG_JSON = `{
|
|
151
|
+
"compilerOptions": {
|
|
152
|
+
"target": "ES2022",
|
|
153
|
+
"moduleResolution": "Node",
|
|
154
|
+
"experimentalDecorators": true,
|
|
155
|
+
"strict": true,
|
|
156
|
+
"skipLibCheck": true,
|
|
157
|
+
"paths": {
|
|
158
|
+
"@dagger.io/dagger": ["./sdk/index.ts"],
|
|
159
|
+
"@dagger.io/dagger/telemetry": ["./sdk/telemetry.ts"]
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
`;
|
|
164
|
+
const YARN_LOCK = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
|
165
|
+
# yarn lockfile v1
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
typescript@5.9.3:
|
|
169
|
+
version "5.9.3"
|
|
170
|
+
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
|
|
171
|
+
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
|
|
172
|
+
`;
|
|
173
|
+
// The SDK Dagger generates at run time stays out of the SDD's history.
|
|
174
|
+
const GITIGNORE = `/sdk
|
|
175
|
+
/**/node_modules/**
|
|
176
|
+
/**/.pnpm-store/**
|
|
177
|
+
/.env
|
|
178
|
+
`;
|
|
179
|
+
/**
|
|
180
|
+
* The module's files, relative to MODULE_DIR, in a fixed order. Pins default to the versions released
|
|
181
|
+
* with this sdd-cli (M-ADR 0218 §4.6); the parity harness passes candidate versions.
|
|
182
|
+
*/
|
|
183
|
+
export function renderModuleFiles(pins = { verify: getVerifyCliVersion(), maintain: getMaintainCliVersion() }) {
|
|
184
|
+
return [
|
|
185
|
+
[".gitignore", GITIGNORE],
|
|
186
|
+
["dagger.json", DAGGER_JSON],
|
|
187
|
+
["package.json", PACKAGE_JSON],
|
|
188
|
+
["src/index.ts", INDEX_TS.split(VERIFY_PIN).join(pins.verify).split(MAINTAIN_PIN).join(pins.maintain).split(DAGGER_SDK).join('"@dagger.io' + '/dagger"')],
|
|
189
|
+
["tsconfig.json", TSCONFIG_JSON],
|
|
190
|
+
["yarn.lock", YARN_LOCK],
|
|
191
|
+
];
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* sha256 over the module's files as they stand (null content = absent), in the render's order, so
|
|
195
|
+
* sync can tell a module nobody touched from an edited one (M-ADR 0218 §4.7, as for the wrapper).
|
|
196
|
+
*/
|
|
197
|
+
export function moduleSha256(files) {
|
|
198
|
+
const h = createHash("sha256");
|
|
199
|
+
for (const [rel, content] of files) {
|
|
200
|
+
h.update(rel).update("\0").update(content === null ? "\u0001absent" : content).update("\0");
|
|
201
|
+
}
|
|
202
|
+
return h.digest("hex");
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=module.js.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { parse as parseYaml } from "yaml";
|
|
5
|
+
export const BINDING_FILENAME = ".sdd/implementation-binding.yaml";
|
|
6
|
+
/** One binding per implementation, for an SDD that specifies several (M-ADR 0214 §4.9). */
|
|
7
|
+
export const BINDINGS_DIRNAME = ".sdd/implementation-bindings";
|
|
8
|
+
export function bindingPath(targetRepo) {
|
|
9
|
+
return resolve(targetRepo, BINDING_FILENAME);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Every binding file of the SDD, relative to its root: the single-implementation file
|
|
13
|
+
* when present, then each `<implementation>.yaml` in the bindings directory, sorted.
|
|
14
|
+
*/
|
|
15
|
+
export function bindingFiles(targetRepo) {
|
|
16
|
+
const out = [];
|
|
17
|
+
if (existsSync(bindingPath(targetRepo)))
|
|
18
|
+
out.push(BINDING_FILENAME);
|
|
19
|
+
const dir = resolve(targetRepo, BINDINGS_DIRNAME);
|
|
20
|
+
if (existsSync(dir)) {
|
|
21
|
+
for (const f of readdirSync(dir).sort()) {
|
|
22
|
+
if (f.endsWith(".yaml") || f.endsWith(".yml"))
|
|
23
|
+
out.push(`${BINDINGS_DIRNAME}/${f}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
export function hasBinding(targetRepo) {
|
|
29
|
+
return bindingFiles(targetRepo).length > 0;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Flatten the binding's implementation members into derived checks. Each member's `checks[]` become
|
|
33
|
+
* Check objects (run as-is by the extractor core); the member's repo/role/lifecycle ride alongside so
|
|
34
|
+
* the emitted finding records *which implementation* drifted. A member with no checks contributes none
|
|
35
|
+
* (e.g. a decommissioned as-is member you no longer validate).
|
|
36
|
+
*/
|
|
37
|
+
export async function deriveChecksFromBinding(targetRepo) {
|
|
38
|
+
const derived = [];
|
|
39
|
+
// A check id is the conformance_finding primary key within the SDD, so one declared by two
|
|
40
|
+
// bindings would collide there: refused here, where both files can be named.
|
|
41
|
+
const declaredIn = new Map();
|
|
42
|
+
for (const file of bindingFiles(targetRepo)) {
|
|
43
|
+
for (const d of await deriveFromFile(resolve(targetRepo, file))) {
|
|
44
|
+
const prior = declaredIn.get(d.check.id);
|
|
45
|
+
if (prior !== undefined) {
|
|
46
|
+
throw new Error(`conformance check id '${d.check.id}' is declared by both ${prior} and ${file} — check ids are unique within the SDD (M-ADR 0214 §4.9)`);
|
|
47
|
+
}
|
|
48
|
+
declaredIn.set(d.check.id, file);
|
|
49
|
+
derived.push(d);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return derived;
|
|
53
|
+
}
|
|
54
|
+
async function deriveFromFile(path) {
|
|
55
|
+
const raw = parseYaml(await readFile(path, "utf-8"));
|
|
56
|
+
const derived = [];
|
|
57
|
+
for (const impl of raw?.implementations ?? []) {
|
|
58
|
+
const member = {
|
|
59
|
+
repo: impl.repo,
|
|
60
|
+
...(impl.role ? { role: impl.role } : {}),
|
|
61
|
+
...(impl.lifecycle ? { lifecycle: impl.lifecycle } : {}),
|
|
62
|
+
...(impl.stack ? { stack: impl.stack } : {}),
|
|
63
|
+
};
|
|
64
|
+
for (const check of impl.checks ?? []) {
|
|
65
|
+
derived.push({ check, member });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return derived;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=binding.js.map
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sdd-cli context pull` — write the code-repo briefing and MCP_SETUP.md into a
|
|
3
|
+
* code repository from its upstream SDD (M-ADR 0128 §1; M-ADR 0150 §4.3–4.4).
|
|
4
|
+
*
|
|
5
|
+
* The successor to the code-repo half of `generate-claude-md.sh`, scoped by
|
|
6
|
+
* M-ADR 0150 Phase D: no dynamic content is baked in; that is the SDD's MCP
|
|
7
|
+
* server's job. A sibling verb to `generate-briefing`, not an extension of it:
|
|
8
|
+
* `generate-briefing` needs the target to be an SDD (marker, lock, synced
|
|
9
|
+
* templates), and a code repository is none of those. They share the
|
|
10
|
+
* placeholder substitution and the wrapper's marker-block merge.
|
|
11
|
+
*
|
|
12
|
+
* What it writes, and nothing else (M-ADR 0218 §4.3 item 9, a MAINTAIN verb):
|
|
13
|
+
* agent-instructions.md whole file, method-managed
|
|
14
|
+
* CLAUDE.md / AGENTS.md the method-managed block only; adopter content survives
|
|
15
|
+
* MCP_SETUP.md whole file, method-managed
|
|
16
|
+
* Templates are read from the SDD, which carries them via its baseline bundle.
|
|
17
|
+
*/
|
|
18
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
19
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
20
|
+
import { AGENT_INSTRUCTIONS_REL_PATH, WRAPPER_BLOCK_BEGIN, WRAPPER_BLOCK_END, findUnresolvedPlaceholders, harnessWrapperOutputPath, resolveBriefingVariables, splitWrapperTemplate, } from "../briefing/index.js";
|
|
21
|
+
import { readLockFile } from "../lock-file.js";
|
|
22
|
+
import { readRepoKind } from "../repo-kind.js";
|
|
23
|
+
import { writeMarkedBlockFile } from "../sync/yaml-block-merge.js";
|
|
24
|
+
import { defaultSearchRoots, resolveCodeRepoName, resolveSdd } from "./resolve.js";
|
|
25
|
+
export const CODE_REPO_BRIEFING_ROOT = "orchestration/templates/briefing/code-repo";
|
|
26
|
+
export const MCP_SETUP_TEMPLATE = "orchestration/templates/mcp/code-repo/MCP_SETUP.md.hbs";
|
|
27
|
+
export const MCP_SETUP_REL_PATH = "MCP_SETUP.md";
|
|
28
|
+
const WRAPPER_TEMPLATE = {
|
|
29
|
+
"claude-code": "claude-code/CLAUDE.md.hbs",
|
|
30
|
+
"github-copilot-cli": "github-copilot-cli/AGENTS.md.hbs",
|
|
31
|
+
};
|
|
32
|
+
export async function runContextPull(opts, out = (line) => process.stdout.write(line)) {
|
|
33
|
+
const codeRepo = resolve(opts.codeRepo);
|
|
34
|
+
if ((await readRepoKind(codeRepo)) !== null) {
|
|
35
|
+
out(fail(`${codeRepo} is an SDD repository (it has .sdd-repo-kind). context pull writes into a code repository; for an SDD use sdd-cli generate-briefing.`));
|
|
36
|
+
return { exitCode: 1 };
|
|
37
|
+
}
|
|
38
|
+
const repoName = resolveCodeRepoName(codeRepo, opts.repoName);
|
|
39
|
+
const outcome = await resolveSdd({
|
|
40
|
+
repoName,
|
|
41
|
+
sddPath: opts.sddPath,
|
|
42
|
+
searchRoots: opts.searchRoots ?? defaultSearchRoots(codeRepo),
|
|
43
|
+
codeRepo,
|
|
44
|
+
});
|
|
45
|
+
if (outcome.kind === "not-an-sdd") {
|
|
46
|
+
out(fail(`--sdd ${outcome.path} is not an SDD repository (no .sdd-repo-kind).`));
|
|
47
|
+
return { exitCode: 1 };
|
|
48
|
+
}
|
|
49
|
+
if (outcome.kind === "not-found") {
|
|
50
|
+
out(fail(`No SDD lists repository "${repoName}" in its catalogues/repositories.csv (searched ${outcome.searched.length} SDD(s) under ${(opts.searchRoots ?? defaultSearchRoots(codeRepo)).join(", ")}). Pass --sdd <path>, or --repo <id> if this repository is catalogued under another id.`));
|
|
51
|
+
return { exitCode: 1 };
|
|
52
|
+
}
|
|
53
|
+
if (outcome.kind === "ambiguous") {
|
|
54
|
+
out(fail(`Repository "${repoName}" is listed by ${outcome.candidates.length} SDDs: ${outcome.candidates.join(", ")}. Pass --sdd <path> to choose.`));
|
|
55
|
+
return { exitCode: 1 };
|
|
56
|
+
}
|
|
57
|
+
const sddRoot = outcome.sddRoot;
|
|
58
|
+
if (!outcome.catalogued) {
|
|
59
|
+
out(warn(`${basename(sddRoot)}'s catalogues/repositories.csv does not list "${repoName}"; continuing because --sdd was given.`));
|
|
60
|
+
}
|
|
61
|
+
// The SDD's own identity, profile and baseline, exactly as its own briefing renders them.
|
|
62
|
+
const lock = await readLockFile(sddRoot);
|
|
63
|
+
const forwardBase = lock?.bundles.find((b) => b.isForwardBase);
|
|
64
|
+
if (!forwardBase) {
|
|
65
|
+
out(fail(`${sddRoot} has no forward-base bundle in method-baseline.lock; sync it first.`));
|
|
66
|
+
return { exitCode: 1 };
|
|
67
|
+
}
|
|
68
|
+
const sdd = await resolveBriefingVariables({ targetRepo: sddRoot, baselineVersionOverride: forwardBase.bundleVersion });
|
|
69
|
+
const vars = {
|
|
70
|
+
repo_name: repoName,
|
|
71
|
+
sdd_name: sdd.sdd_name,
|
|
72
|
+
sdd_root: sddRoot,
|
|
73
|
+
repo_kind: sdd.repo_kind,
|
|
74
|
+
baseline_version: sdd.baseline_version,
|
|
75
|
+
pull_date: opts.pullDate ?? new Date().toISOString().slice(0, 10),
|
|
76
|
+
// MCP_SETUP.md's own names, kept so the template the bash generator rendered still renders.
|
|
77
|
+
upstream_sdd_name: basename(sddRoot),
|
|
78
|
+
sdd_method_baseline_version: sdd.baseline_version,
|
|
79
|
+
};
|
|
80
|
+
const aiTemplate = join(sddRoot, CODE_REPO_BRIEFING_ROOT, "agent-instructions.md.hbs");
|
|
81
|
+
const wrapperTemplate = join(sddRoot, CODE_REPO_BRIEFING_ROOT, WRAPPER_TEMPLATE[opts.agentHarness]);
|
|
82
|
+
const mcpTemplate = join(sddRoot, MCP_SETUP_TEMPLATE);
|
|
83
|
+
for (const t of [aiTemplate, wrapperTemplate, mcpTemplate]) {
|
|
84
|
+
if (!(await exists(t))) {
|
|
85
|
+
out(fail(`${sddRoot} does not carry ${t.slice(sddRoot.length + 1)}: its method baseline predates context pull. Sync the SDD to a baseline that ships the code-repo templates.`));
|
|
86
|
+
return { exitCode: 1 };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const rendered = {};
|
|
90
|
+
for (const [rel, path] of [[AGENT_INSTRUCTIONS_REL_PATH, aiTemplate], [harnessWrapperOutputPath(opts.agentHarness), wrapperTemplate], [MCP_SETUP_REL_PATH, mcpTemplate]]) {
|
|
91
|
+
const content = substituteAll(await readFile(path, "utf8"), vars);
|
|
92
|
+
const unresolved = findUnresolvedPlaceholders(content);
|
|
93
|
+
if (unresolved.length > 0) {
|
|
94
|
+
out(fail(`${rel}: the SDD's template has placeholders this CLI does not know: ${unresolved.join(", ")}. Upgrade sdd-cli.`));
|
|
95
|
+
return { exitCode: 1 };
|
|
96
|
+
}
|
|
97
|
+
rendered[rel] = content;
|
|
98
|
+
}
|
|
99
|
+
// The wrapper first: it is the only write that can be refused, and a refusal must leave nothing written.
|
|
100
|
+
const wrapperRel = harnessWrapperOutputPath(opts.agentHarness);
|
|
101
|
+
const wrapperPath = join(codeRepo, wrapperRel);
|
|
102
|
+
const existing = (await exists(wrapperPath)) ? await readFile(wrapperPath, "utf8") : null;
|
|
103
|
+
const parts = splitWrapperTemplate(rendered[wrapperRel]);
|
|
104
|
+
const hasMarkers = existing !== null && existing.includes(WRAPPER_BLOCK_BEGIN) && existing.includes(WRAPPER_BLOCK_END);
|
|
105
|
+
let wrapperChanged;
|
|
106
|
+
if (existing !== null && !hasMarkers) {
|
|
107
|
+
// No markers: a pre-briefing-layer file (the bash generator wrote these whole) or adopter-authored.
|
|
108
|
+
if (!opts.allowOverwrite) {
|
|
109
|
+
out(fail(`${wrapperRel} exists without method-managed markers, so it is treated as adopter-authored. Nothing was written. Re-run with --allow-overwrite to replace it with the rendered wrapper (move anything worth keeping below its markers afterwards).`));
|
|
110
|
+
return { exitCode: 1 };
|
|
111
|
+
}
|
|
112
|
+
await writeFile(wrapperPath, rendered[wrapperRel], "utf8");
|
|
113
|
+
wrapperChanged = existing !== rendered[wrapperRel];
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const merge = await writeMarkedBlockFile(wrapperPath, existing === null ? rendered[wrapperRel] : parts.block, { begin: WRAPPER_BLOCK_BEGIN, end: WRAPPER_BLOCK_END }, opts.allowOverwrite, existing === null ? undefined : { pre: parts.pre, post: parts.post });
|
|
117
|
+
wrapperChanged = merge.changed;
|
|
118
|
+
}
|
|
119
|
+
const changed = [];
|
|
120
|
+
if (wrapperChanged)
|
|
121
|
+
changed.push(wrapperRel);
|
|
122
|
+
for (const rel of [AGENT_INSTRUCTIONS_REL_PATH, MCP_SETUP_REL_PATH]) {
|
|
123
|
+
const path = join(codeRepo, rel);
|
|
124
|
+
const before = (await exists(path)) ? await readFile(path, "utf8") : null;
|
|
125
|
+
if (before !== rendered[rel]) {
|
|
126
|
+
await mkdir(dirname(path), { recursive: true });
|
|
127
|
+
await writeFile(path, rendered[rel], "utf8");
|
|
128
|
+
changed.push(rel);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
out(info(`Code repo "${repoName}" is specified by ${sdd.sdd_name} (${sdd.repo_kind}, ${sdd.baseline_version}) at ${sddRoot}.`));
|
|
132
|
+
out(info(changed.length > 0 ? `Wrote ${changed.join(", ")}.` : `Already current: ${AGENT_INSTRUCTIONS_REL_PATH}, ${wrapperRel}, ${MCP_SETUP_REL_PATH}.`));
|
|
133
|
+
return { exitCode: 0 };
|
|
134
|
+
}
|
|
135
|
+
/** `{{name}}` substitution over an open variable set (the briefing renderer's is closed to four). */
|
|
136
|
+
export function substituteAll(template, vars) {
|
|
137
|
+
return template.replace(/\{\{\s*([a-z_][a-z0-9_]*)\s*\}\}/g, (full, key) => (key in vars ? vars[key] : full));
|
|
138
|
+
}
|
|
139
|
+
async function exists(path) {
|
|
140
|
+
try {
|
|
141
|
+
await access(path);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const info = (m) => `[INFO] ${m}\n`;
|
|
149
|
+
const warn = (m) => `[WARN] ${m}\n`;
|
|
150
|
+
const fail = (m) => `[FAIL] ${m}\n`;
|
|
151
|
+
//# sourceMappingURL=index.js.map
|