@prodshape/integration-openspec 0.3.1 → 0.4.2
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/dist/index.d.ts +1 -1
- package/dist/index.js +95 -51
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
- package/src/index.ts +35 -47
- package/src/population.ts +13 -23
- package/src/process.ts +84 -0
package/dist/index.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ declare const MIN_SUPPORTED_OPENSPEC = "1.0.0";
|
|
|
40
40
|
*/
|
|
41
41
|
declare const OPENSPEC_NPX_SPEC = "@fission-ai/openspec@1";
|
|
42
42
|
/** The PDaC authority context block injected into openspec/config.yaml `context:`. */
|
|
43
|
-
declare const PDAC_CONTEXT_BLOCK = "<!-- pdac:context begin -->\nThis repository uses Product Definition as Code (PDaC). The canonical product definition lives in docs/product/model (actors, journeys, use cases, business rules, domain terms, requirements)
|
|
43
|
+
declare const PDAC_CONTEXT_BLOCK = "<!-- pdac:context begin -->\nThis repository uses Product Definition as Code (PDaC). The canonical product definition lives in docs/product/model (actors, journeys, use cases, business rules, domain terms, requirements). OpenSpec artifacts consume it and are never a second source of truth.\nA citation binds consumer text to a canonical artifact: it records the artifact id, a sha256 content digest and an optional verification scenario anchor. Verification reports one status per citation: current, stale, tampered or unresolved.\nTo cite: run `npx prodshape inspect <ID>` to read the current digest, then `npx prodshape cite --id <ID> --digest <digest>` to emit the citation record. Place inline citations on their own line directly under the text they ground.\nTo find which artifacts a change impacts, compare the change's intent with the whole product definition first, then widen the result with `npx prodshape impact <ID>`. When the intent contradicts or goes beyond the definition, record that drift in the proposal with the marker `<!-- pdac-drift ids=\"...\" summary=\"...\" -->` (listed by `npx prodshape drift`) and let humans decide \u2014 never fix it quietly.\nEvery current OpenSpec document (proposal, specs, design, tasks and openspec/specs/) has exactly one scope state: bound (it carries at least one citation, or declares `pdac-scope: cited`), exempt (a human declared `pdac-scope: none` because the document has no product-semantic dependency), or unclassified (neither declared \u2014 a verification failure). Binding and exemption are human declarations: never declare `pdac-scope: none` merely because citations are missing, and a `pdac-scope: cited` document with zero citations fails. Verify with `npx prodshape citations verify --provider openspec`: it enumerates the expected documents, so zero discovered citations is a set of failures, never a pass. The scope gate applies to current documents only; citations in archived changes are still checked, with problems reported as warnings, because archived history cannot be edited.\nThe accepted Product Definition changes only through a Product Change under docs/product/changes/: validate it as an overlay while the baseline stays untouched, obtain human product approval, apply explicitly on a working branch, and let a human accept the resulting baseline by merging its pull request. A Product Change is not a pull request; apply is not acceptance; neither apply nor merge attests implementation, verification, release or deployment. OpenSpec changes never edit docs/product/model directly: an OpenSpec change may share a pull request with an applied Product Change or follow later, but it never supplies Product Change status.\n<!-- pdac:context end -->";
|
|
44
44
|
/** The PDaC citation rules injected per artifact into openspec/config.yaml `rules:`. */
|
|
45
45
|
declare const PDAC_RULES: Record<string, string[]>;
|
|
46
46
|
/** The PDaC operation guidance injected into openspec/config.yaml `operations:`. */
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,66 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { execFile as execFile2 } from "child_process";
|
|
3
2
|
import { mkdir, readFile as readFile2, rm, writeFile } from "fs/promises";
|
|
4
3
|
import { dirname, join as join3 } from "path";
|
|
5
4
|
import { parse, stringify } from "yaml";
|
|
6
5
|
|
|
6
|
+
// src/process.ts
|
|
7
|
+
import crossSpawn from "cross-spawn";
|
|
8
|
+
function renderCommand(command, args) {
|
|
9
|
+
return [command, ...args].map((value) => /^[A-Za-z0-9_./:@=+-]+$/.test(value) ? value : JSON.stringify(value)).join(" ");
|
|
10
|
+
}
|
|
11
|
+
function runCommand(command, args, options = {}) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const maxBuffer = options.maxBuffer ?? 10 * 1024 * 1024;
|
|
14
|
+
const child = crossSpawn(command, args, {
|
|
15
|
+
cwd: options.cwd,
|
|
16
|
+
env: options.env,
|
|
17
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
18
|
+
windowsHide: true
|
|
19
|
+
});
|
|
20
|
+
let stdout = "";
|
|
21
|
+
let stderr = "";
|
|
22
|
+
let settled = false;
|
|
23
|
+
const fail = (error) => {
|
|
24
|
+
if (settled) return;
|
|
25
|
+
settled = true;
|
|
26
|
+
reject(error);
|
|
27
|
+
};
|
|
28
|
+
const append = (stream, chunk) => {
|
|
29
|
+
if (settled) return;
|
|
30
|
+
if (stream === "stdout") stdout += chunk;
|
|
31
|
+
else stderr += chunk;
|
|
32
|
+
if (Buffer.byteLength(stdout) + Buffer.byteLength(stderr) > maxBuffer) {
|
|
33
|
+
child.kill();
|
|
34
|
+
fail(
|
|
35
|
+
new Error(`${renderCommand(command, args)} exceeded its ${maxBuffer}-byte output limit`)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
child.stdout?.setEncoding("utf8");
|
|
40
|
+
child.stderr?.setEncoding("utf8");
|
|
41
|
+
child.stdout?.on("data", (chunk) => append("stdout", chunk));
|
|
42
|
+
child.stderr?.on("data", (chunk) => append("stderr", chunk));
|
|
43
|
+
child.once("error", (error) => {
|
|
44
|
+
fail(new Error(`Could not start ${renderCommand(command, args)}: ${error.message}`));
|
|
45
|
+
});
|
|
46
|
+
child.once("close", (code, signal) => {
|
|
47
|
+
if (settled) return;
|
|
48
|
+
if (code === 0) {
|
|
49
|
+
settled = true;
|
|
50
|
+
resolve({ stdout, stderr });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const status = code === null ? `signal ${signal ?? "unknown"}` : `exit ${code}`;
|
|
54
|
+
const detail = stderr.trim() || stdout.trim();
|
|
55
|
+
fail(
|
|
56
|
+
new Error(
|
|
57
|
+
`${renderCommand(command, args)} failed with ${status}${detail ? `: ${detail}` : ""}`
|
|
58
|
+
)
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
7
64
|
// src/workspace.ts
|
|
8
65
|
import { access } from "fs/promises";
|
|
9
66
|
import { join } from "path";
|
|
@@ -34,22 +91,18 @@ function envWithLocalBin(root) {
|
|
|
34
91
|
}
|
|
35
92
|
|
|
36
93
|
// src/population.ts
|
|
37
|
-
import { execFile } from "child_process";
|
|
38
94
|
import { readdir, readFile } from "fs/promises";
|
|
39
95
|
import { join as join2, relative, sep } from "path";
|
|
40
|
-
import { promisify } from "util";
|
|
41
96
|
import fg from "fast-glob";
|
|
42
97
|
import { codes } from "@prodshape/core";
|
|
43
|
-
var execFileAsync = promisify(execFile);
|
|
44
98
|
var CHANGE_ARTIFACT_FILES = [
|
|
45
99
|
{ file: "proposal.md", kind: "proposal" },
|
|
46
100
|
{ file: "design.md", kind: "design" },
|
|
47
101
|
{ file: "tasks.md", kind: "tasks" }
|
|
48
102
|
];
|
|
49
103
|
async function runOpenSpec(repoRoot, args) {
|
|
50
|
-
return
|
|
104
|
+
return runCommand("openspec", args, {
|
|
51
105
|
cwd: repoRoot,
|
|
52
|
-
shell: true,
|
|
53
106
|
env: envWithLocalBin(repoRoot),
|
|
54
107
|
maxBuffer: 10 * 1024 * 1024
|
|
55
108
|
});
|
|
@@ -66,9 +119,8 @@ async function openspecListChanges(repoRoot) {
|
|
|
66
119
|
}
|
|
67
120
|
async function isOpenSpecCliAvailable(repoRoot) {
|
|
68
121
|
try {
|
|
69
|
-
const { stdout } = await
|
|
70
|
-
|
|
71
|
-
...repoRoot ? { env: envWithLocalBin(repoRoot) } : {}
|
|
122
|
+
const { stdout } = await runCommand("openspec", ["--version"], {
|
|
123
|
+
...repoRoot ? { cwd: repoRoot, env: envWithLocalBin(repoRoot) } : {}
|
|
72
124
|
});
|
|
73
125
|
return /\d+\.\d+\.\d+/.test(stdout);
|
|
74
126
|
} catch {
|
|
@@ -146,7 +198,7 @@ async function enumerateOpenSpecDocuments(repoRoot, options) {
|
|
|
146
198
|
const specsDir = join2(root, "specs");
|
|
147
199
|
const documents = [];
|
|
148
200
|
const diagnostics = [];
|
|
149
|
-
let currentChangeNames = await openspecListChanges(repoRoot);
|
|
201
|
+
let currentChangeNames = await pathExists(changesDir) ? await openspecListChanges(repoRoot) : [];
|
|
150
202
|
if (currentChangeNames === null) {
|
|
151
203
|
diagnostics.push({
|
|
152
204
|
severity: "error",
|
|
@@ -201,20 +253,23 @@ var OPENSPEC_NPX_SPEC = "@fission-ai/openspec@1";
|
|
|
201
253
|
var PDAC_CONTEXT_BEGIN = "<!-- pdac:context begin -->";
|
|
202
254
|
var PDAC_CONTEXT_END = "<!-- pdac:context end -->";
|
|
203
255
|
var PDAC_CONTEXT_BLOCK = `${PDAC_CONTEXT_BEGIN}
|
|
204
|
-
This repository uses Product Definition as Code (PDaC). The canonical product definition lives in docs/product/model (actors, journeys, use cases, business rules, domain terms, requirements)
|
|
205
|
-
|
|
256
|
+
This repository uses Product Definition as Code (PDaC). The canonical product definition lives in docs/product/model (actors, journeys, use cases, business rules, domain terms, requirements). OpenSpec artifacts consume it and are never a second source of truth.
|
|
257
|
+
A citation binds consumer text to a canonical artifact: it records the artifact id, a sha256 content digest and an optional verification scenario anchor. Verification reports one status per citation: current, stale, tampered or unresolved.
|
|
206
258
|
To cite: run \`npx prodshape inspect <ID>\` to read the current digest, then \`npx prodshape cite --id <ID> --digest <digest>\` to emit the citation record. Place inline citations on their own line directly under the text they ground.
|
|
207
|
-
|
|
208
|
-
|
|
259
|
+
To find which artifacts a change impacts, compare the change's intent with the whole product definition first, then widen the result with \`npx prodshape impact <ID>\`. When the intent contradicts or goes beyond the definition, record that drift in the proposal with the marker \`<!-- pdac-drift ids="..." summary="..." -->\` (listed by \`npx prodshape drift\`) and let humans decide \u2014 never fix it quietly.
|
|
260
|
+
Every current OpenSpec document (proposal, specs, design, tasks and openspec/specs/) has exactly one scope state: bound (it carries at least one citation, or declares \`pdac-scope: cited\`), exempt (a human declared \`pdac-scope: none\` because the document has no product-semantic dependency), or unclassified (neither declared \u2014 a verification failure). Binding and exemption are human declarations: never declare \`pdac-scope: none\` merely because citations are missing, and a \`pdac-scope: cited\` document with zero citations fails. Verify with \`npx prodshape citations verify --provider openspec\`: it enumerates the expected documents, so zero discovered citations is a set of failures, never a pass. The scope gate applies to current documents only; citations in archived changes are still checked, with problems reported as warnings, because archived history cannot be edited.
|
|
261
|
+
The accepted Product Definition changes only through a Product Change under docs/product/changes/: validate it as an overlay while the baseline stays untouched, obtain human product approval, apply explicitly on a working branch, and let a human accept the resulting baseline by merging its pull request. A Product Change is not a pull request; apply is not acceptance; neither apply nor merge attests implementation, verification, release or deployment. OpenSpec changes never edit docs/product/model directly: an OpenSpec change may share a pull request with an applied Product Change or follow later, but it never supplies Product Change status.
|
|
209
262
|
${PDAC_CONTEXT_END}`;
|
|
210
263
|
var PDAC_RULES = {
|
|
211
264
|
proposal: [
|
|
212
265
|
"State which PDaC artifacts this change touches, each with an inline citation emitted by `npx prodshape cite --id <ID> --digest <digest>`. Never write a citation record by hand.",
|
|
266
|
+
"Find the impacted artifacts as a separate step, before writing any proposal content. Read the whole product definition (docs/product/model by default), compare it with what this change wants \u2014 the backlog item behind it, if there is one \u2014 and list every artifact the change depends on, alters or contradicts. Then widen the list: run `npx prodshape impact <ID>` on each artifact you found and check its direct and indirect neighbours. Put the resulting list in the proposal, cite every impacted artifact from each document that uses it (proposal, specs, design, tasks), and name any neighbour you checked and left out.",
|
|
267
|
+
'If this change\'s goals contradict the product definition, or need behaviour it does not describe, that is product-definition drift. Record it in the proposal under a \'Product definition drift\' note naming the artifacts involved, with the marker `<!-- pdac-drift ids="<ID>[, <ID>...]" summary="<one line>" -->` on its own line so `npx prodshape drift` can list it. The decision is human: the developer and the product owner agree whether to propose a Product Change or adjust this change. Never fix drift quietly, drop or weaken a citation to hide it, or write around the conflict.',
|
|
213
268
|
"If the change implements altered product behaviour, name the Product Change (CHG id) whose applied or accepted artifacts it implements. If no overlay-validated and human-approved Product Change exists yet, stop and ask for one instead of proceeding. The OpenSpec change is not the Product Change.",
|
|
214
269
|
"Every document of this change must end up bound or exempt: bind by citing the canonical text it depends on, or declare `pdac-scope: none` on a line of its own (frontmatter or `<!-- pdac-scope: none -->`) when a human judges the document has no product-semantic dependency. Never declare an exemption just because citations are missing."
|
|
215
270
|
],
|
|
216
271
|
specs: [
|
|
217
|
-
"Every requirement derived from canonical product text MUST carry a citation to every PDaC artifact it derives from
|
|
272
|
+
"Every requirement derived from canonical product text MUST carry a citation to every PDaC artifact it derives from \u2014 not only the closest one \u2014 one citation per line, placed after the requirement text and before the first scenario. Never place citations between the requirement heading and the requirement text; OpenSpec reads the first paragraph as the requirement.",
|
|
218
273
|
'Cite a specific verification scenario with an anchor when the requirement maps to one (anchor="<scenario id>").',
|
|
219
274
|
"Never paraphrase canonical text as if it were new; either cite it inline or embed it in a marker-block citation (<!-- pdac:cite ... --> canonical text <!-- /pdac:cite -->).",
|
|
220
275
|
"Never invent artifact ids or digests. Read them with `npx prodshape inspect <ID>`."
|
|
@@ -277,6 +332,10 @@ function renderCiExample(options) {
|
|
|
277
332
|
"# - unresolved citations, tampered embedded projections, unclassified current documents",
|
|
278
333
|
"# and bound documents with zero citations always fail the gate;",
|
|
279
334
|
...stalePolicy,
|
|
335
|
+
"# - citations in archived changes are checked too, but their problems are reported as",
|
|
336
|
+
"# warnings: archived history cannot be edited, so a problem there is information,",
|
|
337
|
+
"# not something this gate can make anyone fix. Pass --include-archived to apply the",
|
|
338
|
+
"# full gate to archived documents as well.",
|
|
280
339
|
"name: pdac-citations",
|
|
281
340
|
"on:",
|
|
282
341
|
" pull_request:",
|
|
@@ -316,45 +375,30 @@ function compareVersions(a, b) {
|
|
|
316
375
|
if (aMin !== bMin) return aMin - bMin;
|
|
317
376
|
return aPatch - bPatch;
|
|
318
377
|
}
|
|
319
|
-
function runAtRoot(root, command, args) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
args,
|
|
324
|
-
{ cwd: root, shell: true, env: envWithLocalBin(root) },
|
|
325
|
-
(error, stdout, stderr) => {
|
|
326
|
-
if (error) {
|
|
327
|
-
reject(new Error(`${command} ${args.join(" ")} failed: ${error.message}`));
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
resolve(`${stdout}${stderr}`);
|
|
331
|
-
}
|
|
332
|
-
);
|
|
378
|
+
async function runAtRoot(root, command, args) {
|
|
379
|
+
const { stdout, stderr } = await runCommand(command, args, {
|
|
380
|
+
cwd: root,
|
|
381
|
+
env: envWithLocalBin(root)
|
|
333
382
|
});
|
|
383
|
+
return `${stdout}${stderr}`;
|
|
334
384
|
}
|
|
335
385
|
async function detectOpenSpecVersion(root) {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
resolve({ version: match[1] });
|
|
355
|
-
}
|
|
356
|
-
);
|
|
357
|
-
});
|
|
386
|
+
try {
|
|
387
|
+
const { stdout, stderr } = await runCommand("openspec", ["--version"], {
|
|
388
|
+
...root ? { cwd: root, env: envWithLocalBin(root) } : {}
|
|
389
|
+
});
|
|
390
|
+
const output = `${stdout}${stderr}`.trim();
|
|
391
|
+
const match = output.match(/(\d+\.\d+\.\d+(?:-[^\s]+)?)/);
|
|
392
|
+
if (!match?.[1]) {
|
|
393
|
+
return { error: `Could not parse OpenSpec version from output: ${output}` };
|
|
394
|
+
}
|
|
395
|
+
return { version: match[1] };
|
|
396
|
+
} catch (error) {
|
|
397
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
398
|
+
return {
|
|
399
|
+
error: `OpenSpec CLI not found on PATH (run: npm install -g ${OPENSPEC_NPX_SPEC}). ${message}`
|
|
400
|
+
};
|
|
401
|
+
}
|
|
358
402
|
}
|
|
359
403
|
async function bootstrapOpenSpecWorkspace(root) {
|
|
360
404
|
if (await isOpenSpecWorkspace(root)) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/workspace.ts","../src/population.ts"],"sourcesContent":["/**\n * OpenSpec integration for Product Definition as Code.\n *\n * This module configures an existing OpenSpec workspace with PDaC citation rules by merging\n * guidance into `openspec/config.yaml`, and records metadata under `.product/integrations/`.\n * It never creates an OpenSpec plugin, never patches OpenSpec-generated commands or skills, and\n * never forks OpenSpec's default schema. The integration is configuration + deterministic\n * verification: consumer documents cite canonical product artifacts by id + digest, and\n * `prodshape citations verify` checks them.\n *\n * The merge logic is the heart of this module: it preserves every field the user authored\n * (schema, context, rules, operations, githubCopilot, and anything else) and only appends PDaC\n * guidance, deduplicating identical entries so the operation is idempotent.\n */\nimport { execFile } from 'node:child_process';\nimport { mkdir, readFile, rm, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { parse, stringify } from 'yaml';\nimport { envWithLocalBin, isOpenSpecWorkspace, pathExists } from './workspace.js';\n\nexport { isOpenSpecWorkspace } from './workspace.js';\nexport {\n enumerateOpenSpecDocuments,\n isOpenSpecCliAvailable,\n openSpecProvider,\n} from './population.js';\n\n/** The minimum supported OpenSpec CLI version. */\nexport const MIN_SUPPORTED_OPENSPEC = '1.0.0';\n\n/**\n * The npm spec used when bootstrapping OpenSpec through npx. Pinned to the supported major:\n * MIN_SUPPORTED_OPENSPEC guards the floor, the pin guards the ceiling, so a future OpenSpec 2.x\n * cannot silently change what `init --tools none` does for new adopters.\n */\nexport const OPENSPEC_NPX_SPEC = '@fission-ai/openspec@1';\n\n/**\n * Sentinel markers that bracket the PDaC-injected context block. They let `mergeConfig` find\n * and replace exactly the PDaC portion without touching user-authored context lines.\n */\nconst PDAC_CONTEXT_BEGIN = '<!-- pdac:context begin -->';\nconst PDAC_CONTEXT_END = '<!-- pdac:context end -->';\n\n/** The PDaC authority context block injected into openspec/config.yaml `context:`. */\nexport const PDAC_CONTEXT_BLOCK = `${PDAC_CONTEXT_BEGIN}\nThis repository uses Product Definition as Code (PDaC). The canonical product definition lives in docs/product/model (actors, journeys, use cases, business rules, domain terms, requirements) and is the single source of truth for product behaviour. OpenSpec artifacts are downstream consumers of it, never a second source of truth.\nConsumers bind to canonical text through the PDaC Citation Contract: a citation records the target artifact id, a sha256 content digest, and an optional verification scenario anchor. Verification reports one status per citation: current, stale, tampered or unresolved.\nTo cite: run \\`npx prodshape inspect <ID>\\` to read the current digest, then \\`npx prodshape cite --id <ID> --digest <digest>\\` to emit the citation record. Place inline citations on their own line directly under the text they ground.\nEvery current OpenSpec document (proposal, specs, design, tasks and openspec/specs/) has exactly one effective scope state: bound (it carries at least one citation, or declares \\`pdac-scope: cited\\`), exempt (a human declared \\`pdac-scope: none\\` because the document has no product-semantic dependency), or unclassified (neither declared — a verification failure). Binding and exemption are human declarations: never declare \\`pdac-scope: none\\` merely because citations are missing, and a document declared \\`pdac-scope: cited\\` with zero citations fails. Verify with \\`npx prodshape citations verify --provider openspec\\`: it enumerates the expected current document population (archived changes excluded by default), so zero discovered citations is a set of failures, never a pass.\nThe accepted Product Definition changes only through this lifecycle: propose a Product Change under docs/product/changes/, validate its overlay while the baseline remains untouched, obtain human product approval, apply explicitly on a working branch, review the applied result in a pull request, and accept the resulting baseline by human merge. A Product Change is not a pull request. Apply is not acceptance, and neither apply nor merge attests implementation, verification, release or deployment. OpenSpec changes never edit docs/product/model directly. Product-definition work and implementation work have independent cadence: an OpenSpec change may share a pull request with an applied Product Change or follow an accepted definition later, but it remains a downstream implementation concern and never supplies Product Change status.\n${PDAC_CONTEXT_END}`;\n\n/** The PDaC citation rules injected per artifact into openspec/config.yaml `rules:`. */\nexport const PDAC_RULES: Record<string, string[]> = {\n proposal: [\n 'State which PDaC artifacts this change touches, each with an inline citation emitted by `npx prodshape cite --id <ID> --digest <digest>`. Never write a citation record by hand.',\n 'If the change implements altered product behaviour, name the Product Change (CHG id) whose applied or accepted artifacts it implements. If no overlay-validated and human-approved Product Change exists yet, stop and ask for one instead of proceeding. The OpenSpec change is not the Product Change.',\n 'Every document of this change must end up bound or exempt: bind by citing the canonical text it depends on, or declare `pdac-scope: none` on a line of its own (frontmatter or `<!-- pdac-scope: none -->`) when a human judges the document has no product-semantic dependency. Never declare an exemption just because citations are missing.',\n ],\n specs: [\n 'Every requirement derived from canonical product text MUST carry a citation to every PDaC artifact it derives from, one citation per line, placed after the requirement text and before the first scenario. A requirement often derives from more than one artifact; citing only the closest one silently hides the other derivations. Never place citations between the requirement heading and the requirement text; OpenSpec reads the first paragraph as the requirement.',\n 'Cite a specific verification scenario with an anchor when the requirement maps to one (anchor=\"<scenario id>\").',\n 'Never paraphrase canonical text as if it were new; either cite it inline or embed it in a marker-block citation (<!-- pdac:cite ... --> canonical text <!-- /pdac:cite -->).',\n 'Never invent artifact ids or digests. Read them with `npx prodshape inspect <ID>`.',\n ],\n design: [\n 'When a design decision depends on canonical product text, cite the artifact it depends on.',\n ],\n tasks: [\n 'A task that changes cited behaviour must include a follow-up task to refresh the affected citations.',\n 'Never infer implementation, verification, release or deployment from Product Change status; record that evidence in the delivery workflow.',\n ],\n};\n\n/** The PDaC operation guidance injected into openspec/config.yaml `operations:`. */\nexport const PDAC_OPERATIONS: {\n apply: { guidance: string[] };\n archive: { guidance: string[] };\n} = {\n apply: {\n guidance: [\n 'Before finishing, run `npx prodshape citations verify --provider openspec` and fix every stale, tampered or unresolved citation, every unclassified document and every bound document without citations. Refresh a stale digest by re-running `npx prodshape inspect <ID>`; never delete a citation or declare `pdac-scope: none` to silence a diagnostic.',\n ],\n },\n archive: {\n guidance: [\n 'Run `npx prodshape citations verify --provider openspec` one final time. Archive only when every document is bound or exempt and every citation is current.',\n ],\n },\n};\n\n/**\n * The exact strings PDaC injected into openspec/config.yaml, recorded so a later update can\n * remove entries whose wording has since changed instead of appending duplicates alongside them.\n * The context block needs no record: its sentinels already identify it in place.\n */\nexport interface ManagedStrings {\n rules: Record<string, string[]>;\n operations: Record<string, string[]>;\n}\n\n/** The managed strings the current PDaC guidance injects. */\nexport function currentManagedStrings(): ManagedStrings {\n return {\n rules: Object.fromEntries(Object.entries(PDAC_RULES).map(([key, list]) => [key, [...list]])),\n operations: Object.fromEntries(\n Object.entries(PDAC_OPERATIONS).map(([key, op]) => [key, [...op.guidance]]),\n ),\n };\n}\n\n/** Metadata recorded under .product/integrations/openspec.json. */\nexport interface OpenSpecIntegrationMeta {\n provider: 'openspec';\n version: string;\n openspecVersion: string;\n installedAt: string;\n configPath: string;\n /** Absent in metadata written before the CI example existed; `integration update` adds it. */\n ciExamplePath?: string;\n /** Absent in metadata written before the field existed; treated as the current guidance. */\n managed?: ManagedStrings;\n}\n\n/** Repository-relative paths used by this integration. */\nconst CONFIG_RELATIVE = 'openspec/config.yaml';\nconst META_RELATIVE = '.product/integrations/openspec.json';\nconst CI_EXAMPLE_RELATIVE = '.product/integrations/openspec.ci.yml';\n\n/** The repository-relative path of the integration metadata file, for callers that probe it. */\nexport const OPENSPEC_META_RELATIVE = META_RELATIVE;\n\n/** The repository-relative path of the installed CI-ready example workflow. */\nexport const OPENSPEC_CI_EXAMPLE_RELATIVE = CI_EXAMPLE_RELATIVE;\n\n/** The exact provider-aware verification command the integration stands behind. */\nexport const OPENSPEC_VERIFY_COMMAND = 'npx prodshape citations verify --provider openspec';\n\n/**\n * Render the CI-ready example workflow installed at {@link OPENSPEC_CI_EXAMPLE_RELATIVE}.\n *\n * The gate always blocks unresolved citations, tampered embedded projections, unclassified\n * current documents and bound documents with zero citations. Stale citations follow the\n * repository's configured warning policy (`validation.warnings-as-errors` in\n * `.product/config.yaml`); the example never changes that policy, but it states explicitly what\n * the repository's current choice means and how to escalate it, so the blocking behaviour is a\n * deliberate decision rather than a silent default.\n */\nexport function renderCiExample(options: { warningsAsErrors: boolean }): string {\n const stalePolicy = options.warningsAsErrors\n ? [\n '# - stale citations BLOCK this gate: .product/config.yaml sets',\n '# `validation.warnings-as-errors: true`, escalating the PRODUCT061 warning to an error.',\n '# The integration never changes that policy; relax it by setting the key to false.',\n ]\n : [\n '# - stale citations DO NOT block this gate: .product/config.yaml sets',\n '# `validation.warnings-as-errors: false`, so PRODUCT061 stays a warning. To make stale',\n '# citations block the merge, set `validation.warnings-as-errors: true` in',\n '# .product/config.yaml. The integration never changes that policy for you.',\n ];\n return [\n '# PDaC citation gate for OpenSpec consumer documents (CI-ready example).',\n '# Managed by `prodshape integration add openspec`; copy it into your pipeline',\n '# (e.g. .github/workflows/) and adapt the triggers.',\n '#',\n '# The gate runs ProductShape citation and scope verification only. It never invokes',\n \"# `openspec validate`; keep OpenSpec's own verdict a separate step so a native-spec defect\",\n '# and a grounding defect stay distinguishable.',\n '#',\n '# Blocking behaviour:',\n '# - unresolved citations, tampered embedded projections, unclassified current documents',\n '# and bound documents with zero citations always fail the gate;',\n ...stalePolicy,\n 'name: pdac-citations',\n 'on:',\n ' pull_request:',\n 'jobs:',\n ' verify:',\n ' runs-on: ubuntu-latest',\n ' steps:',\n ' - uses: actions/checkout@v4',\n ' - uses: actions/setup-node@v4',\n ' with:',\n \" node-version: '22'\",\n ' # The OpenSpec CLI ships as @fission-ai/openspec; population discovery runs',\n ' # `openspec list`, so the gate needs it installed.',\n ` - run: npm install -g ${OPENSPEC_NPX_SPEC}`,\n ` - run: ${OPENSPEC_VERIFY_COMMAND}`,\n '',\n ].join('\\n');\n}\n\nfunction configPath(root: string): string {\n return join(root, ...CONFIG_RELATIVE.split('/'));\n}\n\nfunction metaPath(root: string): string {\n return join(root, ...META_RELATIVE.split('/'));\n}\n\nfunction ciExamplePath(root: string): string {\n return join(root, ...CI_EXAMPLE_RELATIVE.split('/'));\n}\n\n/**\n * Compare two semver-like version strings (major.minor.patch, ignoring pre-release suffixes).\n * Returns negative if a < b, 0 if equal, positive if a > b.\n */\nfunction compareVersions(a: string, b: string): number {\n const parseParts = (v: string): [number, number, number] => {\n const core = v.split('-')[0]?.split('+')[0] ?? '';\n const segments = core.split('.').map((s) => Number.parseInt(s, 10));\n return [segments[0] ?? 0, segments[1] ?? 0, segments[2] ?? 0];\n };\n const [aMaj, aMin, aPatch] = parseParts(a);\n const [bMaj, bMin, bPatch] = parseParts(b);\n if (aMaj !== bMaj) return aMaj - bMaj;\n if (aMin !== bMin) return aMin - bMin;\n return aPatch - bPatch;\n}\n\n/** Run a command at the repository root, resolving stdout+stderr or rejecting on failure. */\nfunction runAtRoot(root: string, command: string, args: string[]): Promise<string> {\n return new Promise((resolve, reject) => {\n // Commands are typically .cmd shims on Windows; `shell: true` lets execFile find them on PATH\n // across platforms without callers needing to know the extension.\n execFile(\n command,\n args,\n { cwd: root, shell: true, env: envWithLocalBin(root) },\n (error, stdout, stderr) => {\n if (error) {\n reject(new Error(`${command} ${args.join(' ')} failed: ${error.message}`));\n return;\n }\n resolve(`${stdout}${stderr}`);\n },\n );\n });\n}\n\n/**\n * Detect the installed OpenSpec CLI version by running `openspec --version`. When a repository\n * root is given, its node_modules/.bin is consulted first, so a devDependency install counts.\n */\nexport async function detectOpenSpecVersion(\n root?: string,\n): Promise<{ version: string } | { error: string }> {\n return new Promise((resolve) => {\n // `openspec` is typically a .cmd shim on Windows; `shell: true` lets execFile find it on PATH\n // across platforms without callers needing to know the extension.\n execFile(\n 'openspec',\n ['--version'],\n { shell: true, ...(root ? { env: envWithLocalBin(root) } : {}) },\n (error, stdout, stderr) => {\n if (error) {\n resolve({\n error: `OpenSpec CLI not found on PATH (run: npm install -g ${OPENSPEC_NPX_SPEC}). ${error.message}`,\n });\n return;\n }\n const output = `${stdout}${stderr}`.trim();\n // `openspec --version` prints something like \"1.3.0\" or \"openspec/1.3.0 ...\".\n const match = output.match(/(\\d+\\.\\d+\\.\\d+(?:-[^\\s]+)?)/);\n if (!match?.[1]) {\n resolve({ error: `Could not parse OpenSpec version from output: ${output}` });\n return;\n }\n resolve({ version: match[1] });\n },\n );\n });\n}\n\n/**\n * Create an OpenSpec workspace at the root. Uses the installed CLI when one is on PATH (or in\n * node_modules/.bin); otherwise runs the pinned OpenSpec major through npx, which downloads\n * nothing permanent and never mutates the global environment. `--tools none` keeps the\n * framework's own initializer non-interactive.\n *\n * Returns the CLI version that ran and whether it came through npx, so callers can pass the\n * version on to `addOpenSpecIntegration` (nothing may be on PATH afterwards) and recommend a\n * permanent install.\n */\nexport async function bootstrapOpenSpecWorkspace(\n root: string,\n): Promise<{ version: string; viaNpx: boolean; command: string }> {\n if (await isOpenSpecWorkspace(root)) {\n throw new Error('An OpenSpec workspace already exists at this root.');\n }\n const detected = await detectOpenSpecVersion(root);\n if (!('error' in detected)) {\n if (compareVersions(detected.version, MIN_SUPPORTED_OPENSPEC) < 0) {\n throw new Error(\n `OpenSpec CLI version ${detected.version} is below the minimum supported ${MIN_SUPPORTED_OPENSPEC}. Please upgrade OpenSpec.`,\n );\n }\n const command = 'openspec init --tools none';\n await runAtRoot(root, 'openspec', ['init', '--tools', 'none']);\n return { version: detected.version, viaNpx: false, command };\n }\n const command = `npx -y ${OPENSPEC_NPX_SPEC} init --tools none`;\n await runAtRoot(root, 'npx', ['-y', OPENSPEC_NPX_SPEC, 'init', '--tools', 'none']);\n // A second npx call resolves from the cache the first one populated.\n const output = await runAtRoot(root, 'npx', ['-y', OPENSPEC_NPX_SPEC, '--version']);\n const match = output.trim().match(/(\\d+\\.\\d+\\.\\d+(?:-[^\\s]+)?)/);\n if (!match?.[1]) {\n throw new Error(`Could not parse OpenSpec version from npx output: ${output.trim()}`);\n }\n return { version: match[1], viaNpx: true, command };\n}\n\n/** Check if the OpenSpec integration is installed (its metadata file is present). */\nexport async function isOpenSpecIntegrationInstalled(root: string): Promise<boolean> {\n return pathExists(metaPath(root));\n}\n\n/** Read and parse the existing openspec/config.yaml, or null if it does not exist. */\nexport async function readOpenSpecConfig(root: string): Promise<Record<string, unknown> | null> {\n const path = configPath(root);\n if (!(await pathExists(path))) return null;\n const text = await readFile(path, 'utf8');\n const parsed = parse(text);\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n // An empty or non-mapping file: treat as an empty config so we can merge into it.\n return {};\n }\n return parsed as Record<string, unknown>;\n}\n\n/**\n * Extract the PDaC context block (between sentinels) from a context string, or null if absent.\n */\nfunction extractPdacContext(context: string | undefined): string | null {\n if (!context) return null;\n const begin = context.indexOf(PDAC_CONTEXT_BEGIN);\n const end = context.indexOf(PDAC_CONTEXT_END);\n if (begin === -1 || end === -1 || end < begin) return null;\n return context.slice(begin, end + PDAC_CONTEXT_END.length);\n}\n\n/**\n * Merge PDaC guidance into the OpenSpec config, preserving existing content.\n *\n * - `schema` is preserved as-is.\n * - `context`: if the existing context already contains the PDaC block, it is replaced in place\n * (idempotent); otherwise the PDaC block is appended after a separator. User-authored context\n * lines are never lost.\n * - `rules`: for each PDaC artifact (proposal, specs, design, tasks), PDaC rules are appended to\n * existing rules, deduplicating identical entries. Entries recorded as previously managed whose\n * wording is no longer current are removed first, so a guidance change replaces the old string\n * instead of accumulating next to it. User-authored entries are never removed.\n * - `operations`: PDaC apply/archive guidance is appended to existing guidance, deduplicating,\n * with the same previously-managed replacement.\n * - Any other fields (githubCopilot, etc.) are preserved untouched.\n *\n * Returns the merged config and a list of human-readable change descriptions.\n */\nexport function mergeConfig(\n existing: Record<string, unknown> | null,\n previouslyManaged?: ManagedStrings,\n): {\n config: Record<string, unknown>;\n changes: string[];\n} {\n const config: Record<string, unknown> = { ...(existing ?? {}) };\n const changes: string[] = [];\n\n // Strings PDaC injected in the past but no longer injects verbatim: remove before appending.\n const staleManaged = (recorded: string[] | undefined, current: string[]): string[] =>\n (recorded ?? []).filter((entry) => !current.includes(entry));\n\n // --- context ---\n const existingContext =\n typeof config['context'] === 'string' ? (config['context'] as string) : undefined;\n const existingPdacBlock = extractPdacContext(existingContext);\n if (existingPdacBlock !== null) {\n // Replace the existing PDaC block in place (keeps it idempotent and up to date).\n if (existingPdacBlock !== PDAC_CONTEXT_BLOCK) {\n const replaced = existingContext!.replace(existingPdacBlock, PDAC_CONTEXT_BLOCK);\n if (replaced !== existingContext) {\n config['context'] = replaced;\n changes.push('Updated PDaC context block in openspec/config.yaml (context:).');\n }\n }\n // If identical, no change recorded — idempotent.\n } else if (existingContext) {\n // User has their own context: append the PDaC block after a blank-line separator.\n const separator = existingContext.endsWith('\\n') ? '\\n' : '\\n\\n';\n config['context'] = `${existingContext}${separator}${PDAC_CONTEXT_BLOCK}`;\n changes.push('Appended PDaC context block to existing openspec/config.yaml context:.');\n } else {\n config['context'] = PDAC_CONTEXT_BLOCK;\n changes.push('Added PDaC context block to openspec/config.yaml (context:).');\n }\n\n // --- rules ---\n const existingRules =\n config['rules'] && typeof config['rules'] === 'object' && !Array.isArray(config['rules'])\n ? { ...(config['rules'] as Record<string, unknown>) }\n : {};\n let rulesChanged = false;\n let staleRulesRemoved = false;\n const ruleArtifacts = new Set([\n ...Object.keys(PDAC_RULES),\n ...Object.keys(previouslyManaged?.rules ?? {}),\n ]);\n for (const artifact of ruleArtifacts) {\n const pdacRules = PDAC_RULES[artifact] ?? [];\n const stale = staleManaged(previouslyManaged?.rules[artifact], pdacRules);\n const existingList = Array.isArray(existingRules[artifact])\n ? [...(existingRules[artifact] as string[])]\n : [];\n const merged = existingList.filter((entry) => !stale.includes(entry));\n if (merged.length !== existingList.length) {\n staleRulesRemoved = true;\n rulesChanged = true;\n }\n for (const rule of pdacRules) {\n if (!merged.includes(rule)) {\n merged.push(rule);\n rulesChanged = true;\n }\n }\n if (merged.length === 0) {\n delete existingRules[artifact];\n } else {\n existingRules[artifact] = merged;\n }\n }\n if (rulesChanged) {\n if (Object.keys(existingRules).length === 0) {\n delete config['rules'];\n } else {\n config['rules'] = existingRules;\n }\n changes.push(\n staleRulesRemoved\n ? 'Merged PDaC citation rules into openspec/config.yaml (rules:), replacing outdated PDaC entries.'\n : 'Merged PDaC citation rules into openspec/config.yaml (rules:).',\n );\n }\n\n // --- operations ---\n const existingOperations =\n config['operations'] &&\n typeof config['operations'] === 'object' &&\n !Array.isArray(config['operations'])\n ? { ...(config['operations'] as Record<string, unknown>) }\n : {};\n let opsChanged = false;\n let staleOpsRemoved = false;\n for (const opKey of ['apply', 'archive'] as const) {\n const pdacGuidance = PDAC_OPERATIONS[opKey].guidance;\n const stale = staleManaged(previouslyManaged?.operations[opKey], pdacGuidance);\n const existingOp =\n existingOperations[opKey] &&\n typeof existingOperations[opKey] === 'object' &&\n !Array.isArray(existingOperations[opKey])\n ? { ...(existingOperations[opKey] as Record<string, unknown>) }\n : {};\n const existingGuidance = Array.isArray(existingOp['guidance'])\n ? [...(existingOp['guidance'] as string[])]\n : [];\n const merged = existingGuidance.filter((entry) => !stale.includes(entry));\n if (merged.length !== existingGuidance.length) {\n staleOpsRemoved = true;\n opsChanged = true;\n }\n for (const guidance of pdacGuidance) {\n if (!merged.includes(guidance)) {\n merged.push(guidance);\n opsChanged = true;\n }\n }\n existingOp['guidance'] = merged;\n existingOperations[opKey] = existingOp;\n }\n if (opsChanged) {\n config['operations'] = existingOperations;\n changes.push(\n staleOpsRemoved\n ? 'Merged PDaC operation guidance into openspec/config.yaml (operations:), replacing outdated PDaC entries.'\n : 'Merged PDaC operation guidance into openspec/config.yaml (operations:).',\n );\n }\n\n return { config, changes };\n}\n\n/** Serialize config back to YAML with a trailing newline. */\nexport function serializeConfig(config: Record<string, unknown>): string {\n return `${stringify(config).trimEnd()}\\n`;\n}\n\n/** Read the ProductShape version from the integration-openspec package.json. */\nasync function productshapeVersion(): Promise<string> {\n const packageJson = JSON.parse(\n await readFile(new URL('../package.json', import.meta.url), 'utf8'),\n ) as { version: string };\n return packageJson.version;\n}\n\n/**\n * Add the OpenSpec integration to a repository.\n *\n * - Detects the OpenSpec CLI version; rejects if not found or below MIN_SUPPORTED_OPENSPEC.\n * A caller that already ran the CLI (the init bootstrap) can pass `cliVersion` to skip the\n * probe: after an npx bootstrap nothing is on PATH, yet the version is known.\n * - Requires an existing `openspec/` workspace (run `openspec init` first).\n * - Merges PDaC guidance into `openspec/config.yaml`, preserving existing user configuration.\n * - Records metadata under `.product/integrations/openspec.json`, including the exact strings\n * injected, so a later update replaces outdated PDaC entries instead of accumulating them.\n * - Idempotent: running twice produces the same result.\n * - `--dry-run` reports what would change without writing.\n * - `--force` re-merges even if the PDaC block is already present (useful after a PDaC upgrade).\n */\nexport async function addOpenSpecIntegration(\n root: string,\n options: {\n force?: boolean;\n dryRun?: boolean;\n cliVersion?: string;\n /**\n * The repository's configured `validation.warnings-as-errors` policy, reflected verbatim in\n * the installed CI example so its blocking behaviour is stated, never silently changed.\n */\n warningsAsErrors?: boolean;\n } = {},\n): Promise<{ written: string[]; changes: string[]; meta: OpenSpecIntegrationMeta }> {\n const { force = false, dryRun = false, warningsAsErrors = false } = options;\n\n let openspecVersion = options.cliVersion;\n if (!openspecVersion) {\n const detected = await detectOpenSpecVersion(root);\n if ('error' in detected) {\n throw new Error(detected.error);\n }\n openspecVersion = detected.version;\n }\n if (compareVersions(openspecVersion, MIN_SUPPORTED_OPENSPEC) < 0) {\n throw new Error(\n `OpenSpec CLI version ${openspecVersion} is below the minimum supported ${MIN_SUPPORTED_OPENSPEC}. Please upgrade OpenSpec.`,\n );\n }\n\n if (!(await isOpenSpecWorkspace(root))) {\n throw new Error('No OpenSpec workspace found. Run `openspec init` first.');\n }\n\n const existing = await readOpenSpecConfig(root);\n const previousMeta = await readMeta(root);\n\n const { config, changes: configChanges } = mergeConfig(existing, previousMeta?.managed);\n\n const ciDesired = renderCiExample({ warningsAsErrors });\n const ciAbsolute = ciExamplePath(root);\n const ciExisting = (await pathExists(ciAbsolute)) ? await readFile(ciAbsolute, 'utf8') : null;\n const ciChanged = ciExisting !== ciDesired;\n\n const changes = [...configChanges];\n if (ciChanged) {\n changes.push(\n ciExisting === null\n ? `Installed CI-ready verification example at ${CI_EXAMPLE_RELATIVE}.`\n : `Updated CI-ready verification example at ${CI_EXAMPLE_RELATIVE}.`,\n );\n }\n\n const meta: OpenSpecIntegrationMeta = {\n provider: 'openspec',\n version: await productshapeVersion(),\n openspecVersion,\n installedAt: new Date().toISOString(),\n configPath: CONFIG_RELATIVE,\n ciExamplePath: CI_EXAMPLE_RELATIVE,\n managed: currentManagedStrings(),\n };\n\n if (changes.length === 0 && !force) {\n // Already up to date. Still record/refresh metadata.\n if (!dryRun) {\n await writeMeta(root, meta);\n }\n return { written: [], changes: [], meta };\n }\n\n const written: string[] = [];\n if (!dryRun) {\n if (configChanges.length > 0 || force) {\n const serialized = serializeConfig(config);\n await mkdir(dirname(configPath(root)), { recursive: true });\n await writeFile(configPath(root), serialized, 'utf8');\n written.push(CONFIG_RELATIVE);\n }\n if (ciChanged || force) {\n await mkdir(dirname(ciAbsolute), { recursive: true });\n await writeFile(ciAbsolute, ciDesired, 'utf8');\n written.push(CI_EXAMPLE_RELATIVE);\n }\n await writeMeta(root, meta);\n written.push(META_RELATIVE);\n }\n\n return { written, changes, meta };\n}\n\n/** Write the integration metadata file. */\nasync function writeMeta(root: string, meta: OpenSpecIntegrationMeta): Promise<void> {\n const path = metaPath(root);\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, `${JSON.stringify(meta, null, 2)}\\n`, 'utf8');\n}\n\n/** Read the integration metadata file, or null if absent. */\nasync function readMeta(root: string): Promise<OpenSpecIntegrationMeta | null> {\n const path = metaPath(root);\n if (!(await pathExists(path))) return null;\n return JSON.parse(await readFile(path, 'utf8')) as OpenSpecIntegrationMeta;\n}\n\n/**\n * Update (re-merge) the OpenSpec integration. Re-runs the merge so that a PDaC upgrade that\n * changed the context block or rules is reflected. Idempotent.\n */\nexport async function updateOpenSpecIntegration(\n root: string,\n options: { force?: boolean; dryRun?: boolean; warningsAsErrors?: boolean } = {},\n): Promise<{ written: string[]; changes: string[] }> {\n // `add` already re-merges idempotently; `force` ensures the metadata is refreshed even when\n // the config content did not change.\n const result = await addOpenSpecIntegration(root, {\n force: true,\n dryRun: options.dryRun,\n warningsAsErrors: options.warningsAsErrors,\n });\n return { written: result.written, changes: result.changes };\n}\n\n/**\n * Check the OpenSpec integration health. Verifies:\n * - OpenSpec CLI is installed and the version is supported.\n * - `openspec/config.yaml` exists and contains the PDaC context block.\n * - `.product/integrations/openspec.json` metadata exists.\n * - The config's PDaC block matches the current PDaC guidance (not removed or altered).\n */\nexport async function checkOpenSpecIntegration(root: string): Promise<{\n ok: boolean;\n checks: { name: string; ok: boolean; detail: string }[];\n}> {\n const checks: { name: string; ok: boolean; detail: string }[] = [];\n\n // 1. OpenSpec CLI installed and supported.\n const detected = await detectOpenSpecVersion(root);\n if ('error' in detected) {\n checks.push({ name: 'openspec CLI', ok: false, detail: detected.error });\n } else if (compareVersions(detected.version, MIN_SUPPORTED_OPENSPEC) < 0) {\n checks.push({\n name: 'openspec CLI',\n ok: false,\n detail: `OpenSpec ${detected.version} is below minimum supported ${MIN_SUPPORTED_OPENSPEC}.`,\n });\n } else {\n checks.push({\n name: 'openspec CLI',\n ok: true,\n detail: `OpenSpec ${detected.version} detected.`,\n });\n }\n\n // 2. openspec/config.yaml exists and contains PDaC context.\n const config = await readOpenSpecConfig(root);\n if (config === null) {\n checks.push({\n name: 'config.yaml',\n ok: false,\n detail: 'openspec/config.yaml not found. Run: prodshape integration add openspec',\n });\n } else {\n const context =\n typeof config['context'] === 'string' ? (config['context'] as string) : undefined;\n const pdacBlock = extractPdacContext(context);\n if (pdacBlock === null) {\n checks.push({\n name: 'config.yaml',\n ok: false,\n detail:\n 'PDaC context block missing from openspec/config.yaml. Run: prodshape integration add openspec',\n });\n } else if (pdacBlock !== PDAC_CONTEXT_BLOCK) {\n checks.push({\n name: 'config.yaml',\n ok: false,\n detail:\n 'PDaC context block in openspec/config.yaml is outdated. Run: prodshape integration update',\n });\n } else {\n checks.push({\n name: 'config.yaml',\n ok: true,\n detail: 'PDaC context block present and current.',\n });\n }\n }\n\n // 3. Metadata file exists.\n const meta = await readMeta(root);\n if (meta === null) {\n checks.push({\n name: 'metadata',\n ok: false,\n detail:\n '.product/integrations/openspec.json not found. Run: prodshape integration add openspec',\n });\n } else {\n checks.push({\n name: 'metadata',\n ok: true,\n detail: `Integration recorded (OpenSpec ${meta.openspecVersion}, installed ${meta.installedAt}).`,\n });\n }\n\n // 3b. CI-ready verification example present.\n if (await pathExists(ciExamplePath(root))) {\n checks.push({\n name: 'ci example',\n ok: true,\n detail: `CI-ready verification example present at ${CI_EXAMPLE_RELATIVE}.`,\n });\n } else {\n checks.push({\n name: 'ci example',\n ok: false,\n detail: `CI-ready verification example missing (${CI_EXAMPLE_RELATIVE}). Run: prodshape integration update`,\n });\n }\n\n // 4. PDaC rules present.\n if (config) {\n const rules =\n config['rules'] && typeof config['rules'] === 'object' && !Array.isArray(config['rules'])\n ? (config['rules'] as Record<string, unknown>)\n : {};\n const missingRules: string[] = [];\n for (const [artifact, pdacRules] of Object.entries(PDAC_RULES)) {\n const list = Array.isArray(rules[artifact]) ? (rules[artifact] as string[]) : [];\n for (const rule of pdacRules) {\n if (!list.includes(rule)) missingRules.push(artifact);\n }\n }\n if (missingRules.length > 0) {\n checks.push({\n name: 'rules',\n ok: false,\n detail: `PDaC rules missing for: ${[...new Set(missingRules)].join(', ')}. Run: prodshape integration update`,\n });\n } else {\n checks.push({ name: 'rules', ok: true, detail: 'All PDaC citation rules present.' });\n }\n }\n\n const ok = checks.every((c) => c.ok);\n return { ok, checks };\n}\n\n/**\n * Remove the PDaC-injected content from openspec/config.yaml, preserving user-authored content,\n * and delete the metadata file.\n *\n * - Removes the PDaC context block (between sentinels) from `context:`.\n * - Removes PDaC rules from each artifact (keeps user-authored rules).\n * - Removes PDaC operation guidance (keeps user-authored guidance).\n * - Deletes `.product/integrations/openspec.json`.\n * - `--dry-run` reports what would be removed without writing.\n */\nexport async function removeOpenSpecIntegration(\n root: string,\n options: { dryRun?: boolean } = {},\n): Promise<{ removed: string[] }> {\n const { dryRun = false } = options;\n const removed: string[] = [];\n\n // Remove both what the metadata recorded as injected and what the current guidance would\n // inject: the union covers metadata written before the record existed as well as entries left\n // behind by an older ProductShape.\n const recorded = (await readMeta(root))?.managed;\n const current = currentManagedStrings();\n const managedRules: Record<string, string[]> = {};\n for (const key of new Set([\n ...Object.keys(current.rules),\n ...Object.keys(recorded?.rules ?? {}),\n ])) {\n managedRules[key] = [\n ...new Set([...(current.rules[key] ?? []), ...(recorded?.rules[key] ?? [])]),\n ];\n }\n const managedOperations: Record<string, string[]> = {};\n for (const key of new Set([\n ...Object.keys(current.operations),\n ...Object.keys(recorded?.operations ?? {}),\n ])) {\n managedOperations[key] = [\n ...new Set([...(current.operations[key] ?? []), ...(recorded?.operations[key] ?? [])]),\n ];\n }\n\n const config = await readOpenSpecConfig(root);\n if (config !== null) {\n let configChanged = false;\n\n // Remove PDaC context block.\n const context =\n typeof config['context'] === 'string' ? (config['context'] as string) : undefined;\n if (context) {\n const pdacBlock = extractPdacContext(context);\n if (pdacBlock !== null) {\n const cleaned = context\n .replace(pdacBlock, '')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trimEnd();\n if (cleaned === '') {\n delete config['context'];\n } else {\n config['context'] = cleaned;\n }\n configChanged = true;\n }\n }\n\n // Remove PDaC rules from each artifact.\n const rules =\n config['rules'] && typeof config['rules'] === 'object' && !Array.isArray(config['rules'])\n ? (config['rules'] as Record<string, unknown>)\n : null;\n if (rules) {\n let rulesChanged = false;\n for (const [artifact, pdacRules] of Object.entries(managedRules)) {\n if (!Array.isArray(rules[artifact])) continue;\n const list = rules[artifact] as string[];\n const filtered = list.filter((r) => !pdacRules.includes(r));\n if (filtered.length !== list.length) {\n if (filtered.length === 0) {\n delete rules[artifact];\n } else {\n rules[artifact] = filtered;\n }\n rulesChanged = true;\n }\n }\n if (rulesChanged) {\n if (Object.keys(rules).length === 0) {\n delete config['rules'];\n }\n configChanged = true;\n }\n }\n\n // Remove PDaC operation guidance.\n const operations =\n config['operations'] &&\n typeof config['operations'] === 'object' &&\n !Array.isArray(config['operations'])\n ? (config['operations'] as Record<string, unknown>)\n : null;\n if (operations) {\n let opsChanged = false;\n for (const opKey of ['apply', 'archive'] as const) {\n const pdacGuidance = managedOperations[opKey] ?? [];\n const op =\n operations[opKey] &&\n typeof operations[opKey] === 'object' &&\n !Array.isArray(operations[opKey])\n ? (operations[opKey] as Record<string, unknown>)\n : null;\n if (!op || !Array.isArray(op['guidance'])) continue;\n const list = op['guidance'] as string[];\n const filtered = list.filter((g) => !pdacGuidance.includes(g));\n if (filtered.length !== list.length) {\n if (filtered.length === 0) {\n delete op['guidance'];\n } else {\n op['guidance'] = filtered;\n }\n opsChanged = true;\n }\n if (Object.keys(op).length === 0) {\n delete operations[opKey];\n }\n }\n if (opsChanged) {\n if (Object.keys(operations).length === 0) {\n delete config['operations'];\n }\n configChanged = true;\n }\n }\n\n if (configChanged && !dryRun) {\n const serialized = serializeConfig(config);\n await writeFile(configPath(root), serialized, 'utf8');\n removed.push(CONFIG_RELATIVE);\n } else if (configChanged && dryRun) {\n removed.push(CONFIG_RELATIVE);\n }\n }\n\n // Remove the installed CI example.\n if (await pathExists(ciExamplePath(root))) {\n if (!dryRun) {\n await rm(ciExamplePath(root), { force: true });\n }\n removed.push(CI_EXAMPLE_RELATIVE);\n }\n\n // Remove metadata file.\n if (await pathExists(metaPath(root))) {\n if (!dryRun) {\n await rm(metaPath(root), { force: true });\n }\n removed.push(META_RELATIVE);\n }\n\n return { removed };\n}\n","/**\n * Shared workspace helpers for the OpenSpec integration: workspace detection and the\n * child-process environment that lets a devDependency-installed OpenSpec CLI count as installed.\n */\nimport { access } from 'node:fs/promises';\nimport { join } from 'node:path';\n\n/** Check whether a filesystem path exists. */\nexport async function pathExists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n\n/** Check if an OpenSpec workspace exists at the given root (openspec/ directory present). */\nexport async function isOpenSpecWorkspace(root: string): Promise<boolean> {\n return pathExists(join(root, 'openspec'));\n}\n\n/**\n * Build a child-process environment whose PATH starts with the repository's node_modules/.bin,\n * so an OpenSpec installed as a devDependency counts as installed. Windows spells the variable\n * `Path` (and env objects lose the case-insensitive lookup once spread), so every casing is\n * collapsed into a single `PATH` entry.\n */\nexport function envWithLocalBin(root: string): NodeJS.ProcessEnv {\n const env: NodeJS.ProcessEnv = { ...process.env };\n const separator = process.platform === 'win32' ? ';' : ':';\n let existing = '';\n for (const key of Object.keys(env)) {\n if (key.toUpperCase() === 'PATH') {\n existing = env[key] ?? '';\n delete env[key];\n }\n }\n const localBin = join(root, 'node_modules', '.bin');\n env['PATH'] = existing ? `${localBin}${separator}${existing}` : localBin;\n return env;\n}\n","/**\n * OpenSpec consumer-document enumeration: the OpenSpec implementation of the framework-neutral\n * SDD integration-provider contract defined in @prodshape/core.\n *\n * Everything OpenSpec-specific about verification lives here — the `openspec/` layout, the\n * changes/archive lifecycle split, the artifact-file conventions, and the `openspec` CLI — so\n * the core package stays SDD-framework blind. The provider only enumerates the expected current\n * consumer-document population; scope classification and citation verification are core\n * responsibilities that operate on the enumeration.\n */\nimport { execFile } from 'node:child_process';\nimport { readdir, readFile } from 'node:fs/promises';\nimport { join, relative, sep } from 'node:path';\nimport { promisify } from 'node:util';\nimport fg from 'fast-glob';\nimport { codes } from '@prodshape/core';\nimport type {\n ConsumerDocument,\n ConsumerDocumentEnumeration,\n Diagnostic,\n EnumerateConsumerDocumentsOptions,\n SddIntegrationProvider,\n} from '@prodshape/core';\nimport { envWithLocalBin, isOpenSpecWorkspace } from './workspace.js';\n\nconst execFileAsync = promisify(execFile);\n\n/**\n * The artifact files that make up an OpenSpec change, relative to the change directory.\n * `specs/` is a directory containing one or more `<capability>/spec.md` files.\n */\nconst CHANGE_ARTIFACT_FILES: Array<{ file: string; kind: string }> = [\n { file: 'proposal.md', kind: 'proposal' },\n { file: 'design.md', kind: 'design' },\n { file: 'tasks.md', kind: 'tasks' },\n];\n\n/** Run `openspec` with the given args at the repository root, returning stdout. */\nasync function runOpenSpec(\n repoRoot: string,\n args: string[],\n): Promise<{ stdout: string; stderr: string }> {\n // `openspec` is typically a .cmd shim on Windows; `shell: true` lets execFile find it on PATH\n // across platforms. The repository's node_modules/.bin is consulted first, so a\n // devDependency install counts.\n return execFileAsync('openspec', args, {\n cwd: repoRoot,\n shell: true,\n env: envWithLocalBin(repoRoot),\n maxBuffer: 10 * 1024 * 1024,\n });\n}\n\n/** Parsed entry from `openspec list --json`. */\ninterface OpenSpecListEntry {\n name: string;\n}\n\n/**\n * Call `openspec list --json` and return the change names. Returns `null` when the CLI is\n * unavailable or the output is unparseable, so the caller can fall back to filesystem scanning.\n */\nasync function openspecListChanges(repoRoot: string): Promise<string[] | null> {\n try {\n const { stdout } = await runOpenSpec(repoRoot, ['list', '--json']);\n const parsed = JSON.parse(stdout) as { changes?: OpenSpecListEntry[] };\n if (!Array.isArray(parsed.changes)) return null;\n return parsed.changes.map((c) => c.name).filter((n): n is string => typeof n === 'string');\n } catch {\n return null;\n }\n}\n\n/** Check whether `openspec` is runnable (PATH or node_modules/.bin) with a parseable version. */\nexport async function isOpenSpecCliAvailable(repoRoot?: string): Promise<boolean> {\n try {\n const { stdout } = await execFileAsync('openspec', ['--version'], {\n shell: true,\n ...(repoRoot ? { env: envWithLocalBin(repoRoot) } : {}),\n });\n return /\\d+\\.\\d+\\.\\d+/.test(stdout);\n } catch {\n return false;\n }\n}\n\n/** Discover all `.md` files under a directory, returning absolute paths sorted. */\nasync function discoverMarkdownFiles(dir: string): Promise<string[]> {\n const entries = await fg('**/*.md', {\n cwd: dir,\n absolute: true,\n dot: false,\n ignore: ['**/node_modules/**'],\n });\n return entries.map((e) => e.split('/').join(sep)).sort();\n}\n\n/** Build a {@link ConsumerDocument} from an absolute path. */\nfunction buildConsumerDocument(\n absolutePath: string,\n repoRoot: string,\n opts: { change?: string; archived: boolean; artifactKind?: string },\n): ConsumerDocument {\n return {\n path: relative(repoRoot, absolutePath).split(sep).join('/'),\n absolutePath,\n change: opts.change,\n archived: opts.archived,\n artifactKind: opts.artifactKind,\n };\n}\n\n/** Discover artifact files for a single change directory. */\nasync function discoverChangeDocuments(\n changeDir: string,\n repoRoot: string,\n change: string,\n archived: boolean,\n): Promise<ConsumerDocument[]> {\n const docs: ConsumerDocument[] = [];\n\n // Fixed artifact files: proposal.md, design.md, tasks.md.\n for (const { file, kind } of CHANGE_ARTIFACT_FILES) {\n const absolutePath = join(changeDir, file);\n try {\n await readFile(absolutePath);\n docs.push(\n buildConsumerDocument(absolutePath, repoRoot, { change, archived, artifactKind: kind }),\n );\n } catch {\n // File doesn't exist; skip.\n }\n }\n\n // specs/ directory: one or more <capability>/spec.md files.\n const specsDir = join(changeDir, 'specs');\n try {\n const specFiles = await discoverMarkdownFiles(specsDir);\n for (const absolutePath of specFiles) {\n docs.push(\n buildConsumerDocument(absolutePath, repoRoot, { change, archived, artifactKind: 'specs' }),\n );\n }\n } catch {\n // No specs/ directory; skip.\n }\n\n return docs;\n}\n\n/** Discover all `.md` files under `openspec/specs/` (the current specs, not changes). */\nasync function discoverSpecDocuments(\n specsDir: string,\n repoRoot: string,\n): Promise<ConsumerDocument[]> {\n const docs: ConsumerDocument[] = [];\n try {\n const specFiles = await discoverMarkdownFiles(specsDir);\n for (const absolutePath of specFiles) {\n docs.push(\n buildConsumerDocument(absolutePath, repoRoot, { archived: false, artifactKind: 'spec' }),\n );\n }\n } catch {\n // No specs/ directory; skip.\n }\n return docs;\n}\n\n/** Discover archived change directories under `openspec/changes/archive/`. */\nasync function discoverArchivedChanges(archiveDir: string): Promise<string[]> {\n try {\n const entries = await readdir(archiveDir, { withFileTypes: true });\n return entries\n .filter((e) => e.isDirectory())\n .map((e) => e.name)\n .sort();\n } catch {\n return [];\n }\n}\n\n/**\n * Enumerate the consumer-document population of an OpenSpec workspace.\n *\n * Uses `openspec list --json` to determine the current (non-archived) changes. The OpenSpec CLI\n * excludes `openspec/changes/archive/` from `list`, so the default population is current-only.\n * When `includeArchived` is true, archived changes under `openspec/changes/archive/` are also\n * scanned via the filesystem (the CLI cannot address archived changes by name).\n *\n * For each change, the artifact files are resolved by filesystem convention: `proposal.md`,\n * `specs/` (one or more spec files), `design.md`, `tasks.md`. The `openspec show --json` output\n * does not include artifact paths, so convention-based resolution is the stable contract.\n *\n * For `openspec/specs/` (the current specs, not changes), all `.md` files are discovered.\n *\n * If the OpenSpec CLI is not runnable, enumeration falls back to filesystem scanning of\n * `openspec/changes/` (excluding `archive/` unless `includeArchived`) and reports PRODUCT069:\n * the population then rests on layout convention alone, which the gate must surface rather than\n * silently trust.\n */\nexport async function enumerateOpenSpecDocuments(\n repoRoot: string,\n options?: EnumerateConsumerDocumentsOptions,\n): Promise<ConsumerDocumentEnumeration> {\n const includeArchived = options?.includeArchived ?? false;\n const root = join(repoRoot, 'openspec');\n const changesDir = join(root, 'changes');\n const archiveDir = join(changesDir, 'archive');\n const specsDir = join(root, 'specs');\n\n const documents: ConsumerDocument[] = [];\n const diagnostics: Diagnostic[] = [];\n\n // 1. Discover current changes. Prefer `openspec list --json`; fall back to filesystem.\n let currentChangeNames: string[] | null = await openspecListChanges(repoRoot);\n\n if (currentChangeNames === null) {\n diagnostics.push({\n severity: 'error',\n code: codes.openSpecCliMissing,\n message: `OpenSpec CLI not available; the document population was enumerated by filesystem convention instead of 'openspec list'. Install with 'npm install -g @fission-ai/openspec@1' for authoritative population discovery.`,\n file: 'openspec/',\n });\n // CLI unavailable or unparseable: fall back to filesystem scanning of openspec/changes/,\n // excluding archive/ (it's handled separately below).\n try {\n const entries = await readdir(changesDir, { withFileTypes: true });\n currentChangeNames = entries\n .filter((e) => e.isDirectory() && e.name !== 'archive')\n .map((e) => e.name)\n .sort();\n } catch {\n currentChangeNames = [];\n }\n }\n\n // 2. Filter to a single change if requested.\n if (options?.change) {\n const wanted = options.change;\n if (currentChangeNames.includes(wanted)) {\n currentChangeNames = [wanted];\n } else {\n // The requested change isn't in the current list. Check the archive below if asked.\n currentChangeNames = [];\n }\n }\n\n // 3. Discover documents for each current change.\n for (const name of currentChangeNames) {\n const changeDir = join(changesDir, name);\n documents.push(...(await discoverChangeDocuments(changeDir, repoRoot, name, false)));\n }\n\n // 4. Discover archived changes if requested.\n if (includeArchived) {\n const archivedNames = await discoverArchivedChanges(archiveDir);\n for (const name of archivedNames) {\n // If a specific change was requested and it wasn't current, check the archive.\n if (options?.change && name !== options.change) continue;\n const changeDir = join(archiveDir, name);\n documents.push(...(await discoverChangeDocuments(changeDir, repoRoot, name, true)));\n }\n }\n\n // 5. Discover current specs (openspec/specs/).\n documents.push(...(await discoverSpecDocuments(specsDir, repoRoot)));\n\n return {\n documents,\n root: 'openspec',\n includeArchived,\n diagnostics,\n };\n}\n\n/**\n * The OpenSpec implementation of the SDD integration-provider contract.\n * `prodshape citations verify --provider openspec` verifies the population this enumerates.\n */\nexport const openSpecProvider: SddIntegrationProvider = {\n name: 'openspec',\n detectWorkspace: isOpenSpecWorkspace,\n enumerateDocuments: enumerateOpenSpecDocuments,\n};\n"],"mappings":"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","names":["execFile","readFile","join","join","join","join","execFile","command","readFile"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/process.ts","../src/workspace.ts","../src/population.ts"],"sourcesContent":["/**\n * OpenSpec integration for Product Definition as Code.\n *\n * This module configures an existing OpenSpec workspace with PDaC citation rules by merging\n * guidance into `openspec/config.yaml`, and records metadata under `.product/integrations/`.\n * It never creates an OpenSpec plugin, never patches OpenSpec-generated commands or skills, and\n * never forks OpenSpec's default schema. The integration is configuration + deterministic\n * verification: consumer documents cite canonical product artifacts by id + digest, and\n * `prodshape citations verify` checks them.\n *\n * The merge logic is the heart of this module: it preserves every field the user authored\n * (schema, context, rules, operations, githubCopilot, and anything else) and only appends PDaC\n * guidance, deduplicating identical entries so the operation is idempotent.\n */\nimport { mkdir, readFile, rm, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { parse, stringify } from 'yaml';\nimport { runCommand } from './process.js';\nimport { envWithLocalBin, isOpenSpecWorkspace, pathExists } from './workspace.js';\n\nexport { isOpenSpecWorkspace } from './workspace.js';\nexport {\n enumerateOpenSpecDocuments,\n isOpenSpecCliAvailable,\n openSpecProvider,\n} from './population.js';\n\n/** The minimum supported OpenSpec CLI version. */\nexport const MIN_SUPPORTED_OPENSPEC = '1.0.0';\n\n/**\n * The npm spec used when bootstrapping OpenSpec through npx. Pinned to the supported major:\n * MIN_SUPPORTED_OPENSPEC guards the floor, the pin guards the ceiling, so a future OpenSpec 2.x\n * cannot silently change what `init --tools none` does for new adopters.\n */\nexport const OPENSPEC_NPX_SPEC = '@fission-ai/openspec@1';\n\n/**\n * Sentinel markers that bracket the PDaC-injected context block. They let `mergeConfig` find\n * and replace exactly the PDaC portion without touching user-authored context lines.\n */\nconst PDAC_CONTEXT_BEGIN = '<!-- pdac:context begin -->';\nconst PDAC_CONTEXT_END = '<!-- pdac:context end -->';\n\n/** The PDaC authority context block injected into openspec/config.yaml `context:`. */\nexport const PDAC_CONTEXT_BLOCK = `${PDAC_CONTEXT_BEGIN}\nThis repository uses Product Definition as Code (PDaC). The canonical product definition lives in docs/product/model (actors, journeys, use cases, business rules, domain terms, requirements). OpenSpec artifacts consume it and are never a second source of truth.\nA citation binds consumer text to a canonical artifact: it records the artifact id, a sha256 content digest and an optional verification scenario anchor. Verification reports one status per citation: current, stale, tampered or unresolved.\nTo cite: run \\`npx prodshape inspect <ID>\\` to read the current digest, then \\`npx prodshape cite --id <ID> --digest <digest>\\` to emit the citation record. Place inline citations on their own line directly under the text they ground.\nTo find which artifacts a change impacts, compare the change's intent with the whole product definition first, then widen the result with \\`npx prodshape impact <ID>\\`. When the intent contradicts or goes beyond the definition, record that drift in the proposal with the marker \\`<!-- pdac-drift ids=\"...\" summary=\"...\" -->\\` (listed by \\`npx prodshape drift\\`) and let humans decide — never fix it quietly.\nEvery current OpenSpec document (proposal, specs, design, tasks and openspec/specs/) has exactly one scope state: bound (it carries at least one citation, or declares \\`pdac-scope: cited\\`), exempt (a human declared \\`pdac-scope: none\\` because the document has no product-semantic dependency), or unclassified (neither declared — a verification failure). Binding and exemption are human declarations: never declare \\`pdac-scope: none\\` merely because citations are missing, and a \\`pdac-scope: cited\\` document with zero citations fails. Verify with \\`npx prodshape citations verify --provider openspec\\`: it enumerates the expected documents, so zero discovered citations is a set of failures, never a pass. The scope gate applies to current documents only; citations in archived changes are still checked, with problems reported as warnings, because archived history cannot be edited.\nThe accepted Product Definition changes only through a Product Change under docs/product/changes/: validate it as an overlay while the baseline stays untouched, obtain human product approval, apply explicitly on a working branch, and let a human accept the resulting baseline by merging its pull request. A Product Change is not a pull request; apply is not acceptance; neither apply nor merge attests implementation, verification, release or deployment. OpenSpec changes never edit docs/product/model directly: an OpenSpec change may share a pull request with an applied Product Change or follow later, but it never supplies Product Change status.\n${PDAC_CONTEXT_END}`;\n\n/** The PDaC citation rules injected per artifact into openspec/config.yaml `rules:`. */\nexport const PDAC_RULES: Record<string, string[]> = {\n proposal: [\n 'State which PDaC artifacts this change touches, each with an inline citation emitted by `npx prodshape cite --id <ID> --digest <digest>`. Never write a citation record by hand.',\n 'Find the impacted artifacts as a separate step, before writing any proposal content. Read the whole product definition (docs/product/model by default), compare it with what this change wants — the backlog item behind it, if there is one — and list every artifact the change depends on, alters or contradicts. Then widen the list: run `npx prodshape impact <ID>` on each artifact you found and check its direct and indirect neighbours. Put the resulting list in the proposal, cite every impacted artifact from each document that uses it (proposal, specs, design, tasks), and name any neighbour you checked and left out.',\n 'If this change\\'s goals contradict the product definition, or need behaviour it does not describe, that is product-definition drift. Record it in the proposal under a \\'Product definition drift\\' note naming the artifacts involved, with the marker `<!-- pdac-drift ids=\"<ID>[, <ID>...]\" summary=\"<one line>\" -->` on its own line so `npx prodshape drift` can list it. The decision is human: the developer and the product owner agree whether to propose a Product Change or adjust this change. Never fix drift quietly, drop or weaken a citation to hide it, or write around the conflict.',\n 'If the change implements altered product behaviour, name the Product Change (CHG id) whose applied or accepted artifacts it implements. If no overlay-validated and human-approved Product Change exists yet, stop and ask for one instead of proceeding. The OpenSpec change is not the Product Change.',\n 'Every document of this change must end up bound or exempt: bind by citing the canonical text it depends on, or declare `pdac-scope: none` on a line of its own (frontmatter or `<!-- pdac-scope: none -->`) when a human judges the document has no product-semantic dependency. Never declare an exemption just because citations are missing.',\n ],\n specs: [\n 'Every requirement derived from canonical product text MUST carry a citation to every PDaC artifact it derives from — not only the closest one — one citation per line, placed after the requirement text and before the first scenario. Never place citations between the requirement heading and the requirement text; OpenSpec reads the first paragraph as the requirement.',\n 'Cite a specific verification scenario with an anchor when the requirement maps to one (anchor=\"<scenario id>\").',\n 'Never paraphrase canonical text as if it were new; either cite it inline or embed it in a marker-block citation (<!-- pdac:cite ... --> canonical text <!-- /pdac:cite -->).',\n 'Never invent artifact ids or digests. Read them with `npx prodshape inspect <ID>`.',\n ],\n design: [\n 'When a design decision depends on canonical product text, cite the artifact it depends on.',\n ],\n tasks: [\n 'A task that changes cited behaviour must include a follow-up task to refresh the affected citations.',\n 'Never infer implementation, verification, release or deployment from Product Change status; record that evidence in the delivery workflow.',\n ],\n};\n\n/** The PDaC operation guidance injected into openspec/config.yaml `operations:`. */\nexport const PDAC_OPERATIONS: {\n apply: { guidance: string[] };\n archive: { guidance: string[] };\n} = {\n apply: {\n guidance: [\n 'Before finishing, run `npx prodshape citations verify --provider openspec` and fix every stale, tampered or unresolved citation, every unclassified document and every bound document without citations. Refresh a stale digest by re-running `npx prodshape inspect <ID>`; never delete a citation or declare `pdac-scope: none` to silence a diagnostic.',\n ],\n },\n archive: {\n guidance: [\n 'Run `npx prodshape citations verify --provider openspec` one final time. Archive only when every document is bound or exempt and every citation is current.',\n ],\n },\n};\n\n/**\n * The exact strings PDaC injected into openspec/config.yaml, recorded so a later update can\n * remove entries whose wording has since changed instead of appending duplicates alongside them.\n * The context block needs no record: its sentinels already identify it in place.\n */\nexport interface ManagedStrings {\n rules: Record<string, string[]>;\n operations: Record<string, string[]>;\n}\n\n/** The managed strings the current PDaC guidance injects. */\nexport function currentManagedStrings(): ManagedStrings {\n return {\n rules: Object.fromEntries(Object.entries(PDAC_RULES).map(([key, list]) => [key, [...list]])),\n operations: Object.fromEntries(\n Object.entries(PDAC_OPERATIONS).map(([key, op]) => [key, [...op.guidance]]),\n ),\n };\n}\n\n/** Metadata recorded under .product/integrations/openspec.json. */\nexport interface OpenSpecIntegrationMeta {\n provider: 'openspec';\n version: string;\n openspecVersion: string;\n installedAt: string;\n configPath: string;\n /** Absent in metadata written before the CI example existed; `integration update` adds it. */\n ciExamplePath?: string;\n /** Absent in metadata written before the field existed; treated as the current guidance. */\n managed?: ManagedStrings;\n}\n\n/** Repository-relative paths used by this integration. */\nconst CONFIG_RELATIVE = 'openspec/config.yaml';\nconst META_RELATIVE = '.product/integrations/openspec.json';\nconst CI_EXAMPLE_RELATIVE = '.product/integrations/openspec.ci.yml';\n\n/** The repository-relative path of the integration metadata file, for callers that probe it. */\nexport const OPENSPEC_META_RELATIVE = META_RELATIVE;\n\n/** The repository-relative path of the installed CI-ready example workflow. */\nexport const OPENSPEC_CI_EXAMPLE_RELATIVE = CI_EXAMPLE_RELATIVE;\n\n/** The exact provider-aware verification command the integration stands behind. */\nexport const OPENSPEC_VERIFY_COMMAND = 'npx prodshape citations verify --provider openspec';\n\n/**\n * Render the CI-ready example workflow installed at {@link OPENSPEC_CI_EXAMPLE_RELATIVE}.\n *\n * The gate always blocks unresolved citations, tampered embedded projections, unclassified\n * current documents and bound documents with zero citations. Stale citations follow the\n * repository's configured warning policy (`validation.warnings-as-errors` in\n * `.product/config.yaml`); the example never changes that policy, but it states explicitly what\n * the repository's current choice means and how to escalate it, so the blocking behaviour is a\n * deliberate decision rather than a silent default.\n */\nexport function renderCiExample(options: { warningsAsErrors: boolean }): string {\n const stalePolicy = options.warningsAsErrors\n ? [\n '# - stale citations BLOCK this gate: .product/config.yaml sets',\n '# `validation.warnings-as-errors: true`, escalating the PRODUCT061 warning to an error.',\n '# The integration never changes that policy; relax it by setting the key to false.',\n ]\n : [\n '# - stale citations DO NOT block this gate: .product/config.yaml sets',\n '# `validation.warnings-as-errors: false`, so PRODUCT061 stays a warning. To make stale',\n '# citations block the merge, set `validation.warnings-as-errors: true` in',\n '# .product/config.yaml. The integration never changes that policy for you.',\n ];\n return [\n '# PDaC citation gate for OpenSpec consumer documents (CI-ready example).',\n '# Managed by `prodshape integration add openspec`; copy it into your pipeline',\n '# (e.g. .github/workflows/) and adapt the triggers.',\n '#',\n '# The gate runs ProductShape citation and scope verification only. It never invokes',\n \"# `openspec validate`; keep OpenSpec's own verdict a separate step so a native-spec defect\",\n '# and a grounding defect stay distinguishable.',\n '#',\n '# Blocking behaviour:',\n '# - unresolved citations, tampered embedded projections, unclassified current documents',\n '# and bound documents with zero citations always fail the gate;',\n ...stalePolicy,\n '# - citations in archived changes are checked too, but their problems are reported as',\n '# warnings: archived history cannot be edited, so a problem there is information,',\n '# not something this gate can make anyone fix. Pass --include-archived to apply the',\n '# full gate to archived documents as well.',\n 'name: pdac-citations',\n 'on:',\n ' pull_request:',\n 'jobs:',\n ' verify:',\n ' runs-on: ubuntu-latest',\n ' steps:',\n ' - uses: actions/checkout@v4',\n ' - uses: actions/setup-node@v4',\n ' with:',\n \" node-version: '22'\",\n ' # The OpenSpec CLI ships as @fission-ai/openspec; population discovery runs',\n ' # `openspec list`, so the gate needs it installed.',\n ` - run: npm install -g ${OPENSPEC_NPX_SPEC}`,\n ` - run: ${OPENSPEC_VERIFY_COMMAND}`,\n '',\n ].join('\\n');\n}\n\nfunction configPath(root: string): string {\n return join(root, ...CONFIG_RELATIVE.split('/'));\n}\n\nfunction metaPath(root: string): string {\n return join(root, ...META_RELATIVE.split('/'));\n}\n\nfunction ciExamplePath(root: string): string {\n return join(root, ...CI_EXAMPLE_RELATIVE.split('/'));\n}\n\n/**\n * Compare two semver-like version strings (major.minor.patch, ignoring pre-release suffixes).\n * Returns negative if a < b, 0 if equal, positive if a > b.\n */\nfunction compareVersions(a: string, b: string): number {\n const parseParts = (v: string): [number, number, number] => {\n const core = v.split('-')[0]?.split('+')[0] ?? '';\n const segments = core.split('.').map((s) => Number.parseInt(s, 10));\n return [segments[0] ?? 0, segments[1] ?? 0, segments[2] ?? 0];\n };\n const [aMaj, aMin, aPatch] = parseParts(a);\n const [bMaj, bMin, bPatch] = parseParts(b);\n if (aMaj !== bMaj) return aMaj - bMaj;\n if (aMin !== bMin) return aMin - bMin;\n return aPatch - bPatch;\n}\n\n/** Run a command at the repository root, resolving stdout+stderr or rejecting on failure. */\nasync function runAtRoot(root: string, command: string, args: string[]): Promise<string> {\n const { stdout, stderr } = await runCommand(command, args, {\n cwd: root,\n env: envWithLocalBin(root),\n });\n return `${stdout}${stderr}`;\n}\n\n/**\n * Detect the installed OpenSpec CLI version by running `openspec --version`. When a repository\n * root is given, its node_modules/.bin is consulted first, so a devDependency install counts.\n */\nexport async function detectOpenSpecVersion(\n root?: string,\n): Promise<{ version: string } | { error: string }> {\n try {\n const { stdout, stderr } = await runCommand('openspec', ['--version'], {\n ...(root ? { cwd: root, env: envWithLocalBin(root) } : {}),\n });\n const output = `${stdout}${stderr}`.trim();\n // `openspec --version` prints something like \"1.3.0\" or \"openspec/1.3.0 ...\".\n const match = output.match(/(\\d+\\.\\d+\\.\\d+(?:-[^\\s]+)?)/);\n if (!match?.[1]) {\n return { error: `Could not parse OpenSpec version from output: ${output}` };\n }\n return { version: match[1] };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return {\n error: `OpenSpec CLI not found on PATH (run: npm install -g ${OPENSPEC_NPX_SPEC}). ${message}`,\n };\n }\n}\n\n/**\n * Create an OpenSpec workspace at the root. Uses the installed CLI when one is on PATH (or in\n * node_modules/.bin); otherwise runs the pinned OpenSpec major through npx, which downloads\n * nothing permanent and never mutates the global environment. `--tools none` keeps the\n * framework's own initializer non-interactive.\n *\n * Returns the CLI version that ran and whether it came through npx, so callers can pass the\n * version on to `addOpenSpecIntegration` (nothing may be on PATH afterwards) and recommend a\n * permanent install.\n */\nexport async function bootstrapOpenSpecWorkspace(\n root: string,\n): Promise<{ version: string; viaNpx: boolean; command: string }> {\n if (await isOpenSpecWorkspace(root)) {\n throw new Error('An OpenSpec workspace already exists at this root.');\n }\n const detected = await detectOpenSpecVersion(root);\n if (!('error' in detected)) {\n if (compareVersions(detected.version, MIN_SUPPORTED_OPENSPEC) < 0) {\n throw new Error(\n `OpenSpec CLI version ${detected.version} is below the minimum supported ${MIN_SUPPORTED_OPENSPEC}. Please upgrade OpenSpec.`,\n );\n }\n const command = 'openspec init --tools none';\n await runAtRoot(root, 'openspec', ['init', '--tools', 'none']);\n return { version: detected.version, viaNpx: false, command };\n }\n const command = `npx -y ${OPENSPEC_NPX_SPEC} init --tools none`;\n await runAtRoot(root, 'npx', ['-y', OPENSPEC_NPX_SPEC, 'init', '--tools', 'none']);\n // A second npx call resolves from the cache the first one populated.\n const output = await runAtRoot(root, 'npx', ['-y', OPENSPEC_NPX_SPEC, '--version']);\n const match = output.trim().match(/(\\d+\\.\\d+\\.\\d+(?:-[^\\s]+)?)/);\n if (!match?.[1]) {\n throw new Error(`Could not parse OpenSpec version from npx output: ${output.trim()}`);\n }\n return { version: match[1], viaNpx: true, command };\n}\n\n/** Check if the OpenSpec integration is installed (its metadata file is present). */\nexport async function isOpenSpecIntegrationInstalled(root: string): Promise<boolean> {\n return pathExists(metaPath(root));\n}\n\n/** Read and parse the existing openspec/config.yaml, or null if it does not exist. */\nexport async function readOpenSpecConfig(root: string): Promise<Record<string, unknown> | null> {\n const path = configPath(root);\n if (!(await pathExists(path))) return null;\n const text = await readFile(path, 'utf8');\n const parsed = parse(text);\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n // An empty or non-mapping file: treat as an empty config so we can merge into it.\n return {};\n }\n return parsed as Record<string, unknown>;\n}\n\n/**\n * Extract the PDaC context block (between sentinels) from a context string, or null if absent.\n */\nfunction extractPdacContext(context: string | undefined): string | null {\n if (!context) return null;\n const begin = context.indexOf(PDAC_CONTEXT_BEGIN);\n const end = context.indexOf(PDAC_CONTEXT_END);\n if (begin === -1 || end === -1 || end < begin) return null;\n return context.slice(begin, end + PDAC_CONTEXT_END.length);\n}\n\n/**\n * Merge PDaC guidance into the OpenSpec config, preserving existing content.\n *\n * - `schema` is preserved as-is.\n * - `context`: if the existing context already contains the PDaC block, it is replaced in place\n * (idempotent); otherwise the PDaC block is appended after a separator. User-authored context\n * lines are never lost.\n * - `rules`: for each PDaC artifact (proposal, specs, design, tasks), PDaC rules are appended to\n * existing rules, deduplicating identical entries. Entries recorded as previously managed whose\n * wording is no longer current are removed first, so a guidance change replaces the old string\n * instead of accumulating next to it. User-authored entries are never removed.\n * - `operations`: PDaC apply/archive guidance is appended to existing guidance, deduplicating,\n * with the same previously-managed replacement.\n * - Any other fields (githubCopilot, etc.) are preserved untouched.\n *\n * Returns the merged config and a list of human-readable change descriptions.\n */\nexport function mergeConfig(\n existing: Record<string, unknown> | null,\n previouslyManaged?: ManagedStrings,\n): {\n config: Record<string, unknown>;\n changes: string[];\n} {\n const config: Record<string, unknown> = { ...(existing ?? {}) };\n const changes: string[] = [];\n\n // Strings PDaC injected in the past but no longer injects verbatim: remove before appending.\n const staleManaged = (recorded: string[] | undefined, current: string[]): string[] =>\n (recorded ?? []).filter((entry) => !current.includes(entry));\n\n // --- context ---\n const existingContext =\n typeof config['context'] === 'string' ? (config['context'] as string) : undefined;\n const existingPdacBlock = extractPdacContext(existingContext);\n if (existingPdacBlock !== null) {\n // Replace the existing PDaC block in place (keeps it idempotent and up to date).\n if (existingPdacBlock !== PDAC_CONTEXT_BLOCK) {\n const replaced = existingContext!.replace(existingPdacBlock, PDAC_CONTEXT_BLOCK);\n if (replaced !== existingContext) {\n config['context'] = replaced;\n changes.push('Updated PDaC context block in openspec/config.yaml (context:).');\n }\n }\n // If identical, no change recorded — idempotent.\n } else if (existingContext) {\n // User has their own context: append the PDaC block after a blank-line separator.\n const separator = existingContext.endsWith('\\n') ? '\\n' : '\\n\\n';\n config['context'] = `${existingContext}${separator}${PDAC_CONTEXT_BLOCK}`;\n changes.push('Appended PDaC context block to existing openspec/config.yaml context:.');\n } else {\n config['context'] = PDAC_CONTEXT_BLOCK;\n changes.push('Added PDaC context block to openspec/config.yaml (context:).');\n }\n\n // --- rules ---\n const existingRules =\n config['rules'] && typeof config['rules'] === 'object' && !Array.isArray(config['rules'])\n ? { ...(config['rules'] as Record<string, unknown>) }\n : {};\n let rulesChanged = false;\n let staleRulesRemoved = false;\n const ruleArtifacts = new Set([\n ...Object.keys(PDAC_RULES),\n ...Object.keys(previouslyManaged?.rules ?? {}),\n ]);\n for (const artifact of ruleArtifacts) {\n const pdacRules = PDAC_RULES[artifact] ?? [];\n const stale = staleManaged(previouslyManaged?.rules[artifact], pdacRules);\n const existingList = Array.isArray(existingRules[artifact])\n ? [...(existingRules[artifact] as string[])]\n : [];\n const merged = existingList.filter((entry) => !stale.includes(entry));\n if (merged.length !== existingList.length) {\n staleRulesRemoved = true;\n rulesChanged = true;\n }\n for (const rule of pdacRules) {\n if (!merged.includes(rule)) {\n merged.push(rule);\n rulesChanged = true;\n }\n }\n if (merged.length === 0) {\n delete existingRules[artifact];\n } else {\n existingRules[artifact] = merged;\n }\n }\n if (rulesChanged) {\n if (Object.keys(existingRules).length === 0) {\n delete config['rules'];\n } else {\n config['rules'] = existingRules;\n }\n changes.push(\n staleRulesRemoved\n ? 'Merged PDaC citation rules into openspec/config.yaml (rules:), replacing outdated PDaC entries.'\n : 'Merged PDaC citation rules into openspec/config.yaml (rules:).',\n );\n }\n\n // --- operations ---\n const existingOperations =\n config['operations'] &&\n typeof config['operations'] === 'object' &&\n !Array.isArray(config['operations'])\n ? { ...(config['operations'] as Record<string, unknown>) }\n : {};\n let opsChanged = false;\n let staleOpsRemoved = false;\n for (const opKey of ['apply', 'archive'] as const) {\n const pdacGuidance = PDAC_OPERATIONS[opKey].guidance;\n const stale = staleManaged(previouslyManaged?.operations[opKey], pdacGuidance);\n const existingOp =\n existingOperations[opKey] &&\n typeof existingOperations[opKey] === 'object' &&\n !Array.isArray(existingOperations[opKey])\n ? { ...(existingOperations[opKey] as Record<string, unknown>) }\n : {};\n const existingGuidance = Array.isArray(existingOp['guidance'])\n ? [...(existingOp['guidance'] as string[])]\n : [];\n const merged = existingGuidance.filter((entry) => !stale.includes(entry));\n if (merged.length !== existingGuidance.length) {\n staleOpsRemoved = true;\n opsChanged = true;\n }\n for (const guidance of pdacGuidance) {\n if (!merged.includes(guidance)) {\n merged.push(guidance);\n opsChanged = true;\n }\n }\n existingOp['guidance'] = merged;\n existingOperations[opKey] = existingOp;\n }\n if (opsChanged) {\n config['operations'] = existingOperations;\n changes.push(\n staleOpsRemoved\n ? 'Merged PDaC operation guidance into openspec/config.yaml (operations:), replacing outdated PDaC entries.'\n : 'Merged PDaC operation guidance into openspec/config.yaml (operations:).',\n );\n }\n\n return { config, changes };\n}\n\n/** Serialize config back to YAML with a trailing newline. */\nexport function serializeConfig(config: Record<string, unknown>): string {\n return `${stringify(config).trimEnd()}\\n`;\n}\n\n/** Read the ProductShape version from the integration-openspec package.json. */\nasync function productshapeVersion(): Promise<string> {\n const packageJson = JSON.parse(\n await readFile(new URL('../package.json', import.meta.url), 'utf8'),\n ) as { version: string };\n return packageJson.version;\n}\n\n/**\n * Add the OpenSpec integration to a repository.\n *\n * - Detects the OpenSpec CLI version; rejects if not found or below MIN_SUPPORTED_OPENSPEC.\n * A caller that already ran the CLI (the init bootstrap) can pass `cliVersion` to skip the\n * probe: after an npx bootstrap nothing is on PATH, yet the version is known.\n * - Requires an existing `openspec/` workspace (run `openspec init` first).\n * - Merges PDaC guidance into `openspec/config.yaml`, preserving existing user configuration.\n * - Records metadata under `.product/integrations/openspec.json`, including the exact strings\n * injected, so a later update replaces outdated PDaC entries instead of accumulating them.\n * - Idempotent: running twice produces the same result.\n * - `--dry-run` reports what would change without writing.\n * - `--force` re-merges even if the PDaC block is already present (useful after a PDaC upgrade).\n */\nexport async function addOpenSpecIntegration(\n root: string,\n options: {\n force?: boolean;\n dryRun?: boolean;\n cliVersion?: string;\n /**\n * The repository's configured `validation.warnings-as-errors` policy, reflected verbatim in\n * the installed CI example so its blocking behaviour is stated, never silently changed.\n */\n warningsAsErrors?: boolean;\n } = {},\n): Promise<{ written: string[]; changes: string[]; meta: OpenSpecIntegrationMeta }> {\n const { force = false, dryRun = false, warningsAsErrors = false } = options;\n\n let openspecVersion = options.cliVersion;\n if (!openspecVersion) {\n const detected = await detectOpenSpecVersion(root);\n if ('error' in detected) {\n throw new Error(detected.error);\n }\n openspecVersion = detected.version;\n }\n if (compareVersions(openspecVersion, MIN_SUPPORTED_OPENSPEC) < 0) {\n throw new Error(\n `OpenSpec CLI version ${openspecVersion} is below the minimum supported ${MIN_SUPPORTED_OPENSPEC}. Please upgrade OpenSpec.`,\n );\n }\n\n if (!(await isOpenSpecWorkspace(root))) {\n throw new Error('No OpenSpec workspace found. Run `openspec init` first.');\n }\n\n const existing = await readOpenSpecConfig(root);\n const previousMeta = await readMeta(root);\n\n const { config, changes: configChanges } = mergeConfig(existing, previousMeta?.managed);\n\n const ciDesired = renderCiExample({ warningsAsErrors });\n const ciAbsolute = ciExamplePath(root);\n const ciExisting = (await pathExists(ciAbsolute)) ? await readFile(ciAbsolute, 'utf8') : null;\n const ciChanged = ciExisting !== ciDesired;\n\n const changes = [...configChanges];\n if (ciChanged) {\n changes.push(\n ciExisting === null\n ? `Installed CI-ready verification example at ${CI_EXAMPLE_RELATIVE}.`\n : `Updated CI-ready verification example at ${CI_EXAMPLE_RELATIVE}.`,\n );\n }\n\n const meta: OpenSpecIntegrationMeta = {\n provider: 'openspec',\n version: await productshapeVersion(),\n openspecVersion,\n installedAt: new Date().toISOString(),\n configPath: CONFIG_RELATIVE,\n ciExamplePath: CI_EXAMPLE_RELATIVE,\n managed: currentManagedStrings(),\n };\n\n if (changes.length === 0 && !force) {\n // Already up to date. Still record/refresh metadata.\n if (!dryRun) {\n await writeMeta(root, meta);\n }\n return { written: [], changes: [], meta };\n }\n\n const written: string[] = [];\n if (!dryRun) {\n if (configChanges.length > 0 || force) {\n const serialized = serializeConfig(config);\n await mkdir(dirname(configPath(root)), { recursive: true });\n await writeFile(configPath(root), serialized, 'utf8');\n written.push(CONFIG_RELATIVE);\n }\n if (ciChanged || force) {\n await mkdir(dirname(ciAbsolute), { recursive: true });\n await writeFile(ciAbsolute, ciDesired, 'utf8');\n written.push(CI_EXAMPLE_RELATIVE);\n }\n await writeMeta(root, meta);\n written.push(META_RELATIVE);\n }\n\n return { written, changes, meta };\n}\n\n/** Write the integration metadata file. */\nasync function writeMeta(root: string, meta: OpenSpecIntegrationMeta): Promise<void> {\n const path = metaPath(root);\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, `${JSON.stringify(meta, null, 2)}\\n`, 'utf8');\n}\n\n/** Read the integration metadata file, or null if absent. */\nasync function readMeta(root: string): Promise<OpenSpecIntegrationMeta | null> {\n const path = metaPath(root);\n if (!(await pathExists(path))) return null;\n return JSON.parse(await readFile(path, 'utf8')) as OpenSpecIntegrationMeta;\n}\n\n/**\n * Update (re-merge) the OpenSpec integration. Re-runs the merge so that a PDaC upgrade that\n * changed the context block or rules is reflected. Idempotent.\n */\nexport async function updateOpenSpecIntegration(\n root: string,\n options: { force?: boolean; dryRun?: boolean; warningsAsErrors?: boolean } = {},\n): Promise<{ written: string[]; changes: string[] }> {\n // `add` already re-merges idempotently; `force` ensures the metadata is refreshed even when\n // the config content did not change.\n const result = await addOpenSpecIntegration(root, {\n force: true,\n dryRun: options.dryRun,\n warningsAsErrors: options.warningsAsErrors,\n });\n return { written: result.written, changes: result.changes };\n}\n\n/**\n * Check the OpenSpec integration health. Verifies:\n * - OpenSpec CLI is installed and the version is supported.\n * - `openspec/config.yaml` exists and contains the PDaC context block.\n * - `.product/integrations/openspec.json` metadata exists.\n * - The config's PDaC block matches the current PDaC guidance (not removed or altered).\n */\nexport async function checkOpenSpecIntegration(root: string): Promise<{\n ok: boolean;\n checks: { name: string; ok: boolean; detail: string }[];\n}> {\n const checks: { name: string; ok: boolean; detail: string }[] = [];\n\n // 1. OpenSpec CLI installed and supported.\n const detected = await detectOpenSpecVersion(root);\n if ('error' in detected) {\n checks.push({ name: 'openspec CLI', ok: false, detail: detected.error });\n } else if (compareVersions(detected.version, MIN_SUPPORTED_OPENSPEC) < 0) {\n checks.push({\n name: 'openspec CLI',\n ok: false,\n detail: `OpenSpec ${detected.version} is below minimum supported ${MIN_SUPPORTED_OPENSPEC}.`,\n });\n } else {\n checks.push({\n name: 'openspec CLI',\n ok: true,\n detail: `OpenSpec ${detected.version} detected.`,\n });\n }\n\n // 2. openspec/config.yaml exists and contains PDaC context.\n const config = await readOpenSpecConfig(root);\n if (config === null) {\n checks.push({\n name: 'config.yaml',\n ok: false,\n detail: 'openspec/config.yaml not found. Run: prodshape integration add openspec',\n });\n } else {\n const context =\n typeof config['context'] === 'string' ? (config['context'] as string) : undefined;\n const pdacBlock = extractPdacContext(context);\n if (pdacBlock === null) {\n checks.push({\n name: 'config.yaml',\n ok: false,\n detail:\n 'PDaC context block missing from openspec/config.yaml. Run: prodshape integration add openspec',\n });\n } else if (pdacBlock !== PDAC_CONTEXT_BLOCK) {\n checks.push({\n name: 'config.yaml',\n ok: false,\n detail:\n 'PDaC context block in openspec/config.yaml is outdated. Run: prodshape integration update',\n });\n } else {\n checks.push({\n name: 'config.yaml',\n ok: true,\n detail: 'PDaC context block present and current.',\n });\n }\n }\n\n // 3. Metadata file exists.\n const meta = await readMeta(root);\n if (meta === null) {\n checks.push({\n name: 'metadata',\n ok: false,\n detail:\n '.product/integrations/openspec.json not found. Run: prodshape integration add openspec',\n });\n } else {\n checks.push({\n name: 'metadata',\n ok: true,\n detail: `Integration recorded (OpenSpec ${meta.openspecVersion}, installed ${meta.installedAt}).`,\n });\n }\n\n // 3b. CI-ready verification example present.\n if (await pathExists(ciExamplePath(root))) {\n checks.push({\n name: 'ci example',\n ok: true,\n detail: `CI-ready verification example present at ${CI_EXAMPLE_RELATIVE}.`,\n });\n } else {\n checks.push({\n name: 'ci example',\n ok: false,\n detail: `CI-ready verification example missing (${CI_EXAMPLE_RELATIVE}). Run: prodshape integration update`,\n });\n }\n\n // 4. PDaC rules present.\n if (config) {\n const rules =\n config['rules'] && typeof config['rules'] === 'object' && !Array.isArray(config['rules'])\n ? (config['rules'] as Record<string, unknown>)\n : {};\n const missingRules: string[] = [];\n for (const [artifact, pdacRules] of Object.entries(PDAC_RULES)) {\n const list = Array.isArray(rules[artifact]) ? (rules[artifact] as string[]) : [];\n for (const rule of pdacRules) {\n if (!list.includes(rule)) missingRules.push(artifact);\n }\n }\n if (missingRules.length > 0) {\n checks.push({\n name: 'rules',\n ok: false,\n detail: `PDaC rules missing for: ${[...new Set(missingRules)].join(', ')}. Run: prodshape integration update`,\n });\n } else {\n checks.push({ name: 'rules', ok: true, detail: 'All PDaC citation rules present.' });\n }\n }\n\n const ok = checks.every((c) => c.ok);\n return { ok, checks };\n}\n\n/**\n * Remove the PDaC-injected content from openspec/config.yaml, preserving user-authored content,\n * and delete the metadata file.\n *\n * - Removes the PDaC context block (between sentinels) from `context:`.\n * - Removes PDaC rules from each artifact (keeps user-authored rules).\n * - Removes PDaC operation guidance (keeps user-authored guidance).\n * - Deletes `.product/integrations/openspec.json`.\n * - `--dry-run` reports what would be removed without writing.\n */\nexport async function removeOpenSpecIntegration(\n root: string,\n options: { dryRun?: boolean } = {},\n): Promise<{ removed: string[] }> {\n const { dryRun = false } = options;\n const removed: string[] = [];\n\n // Remove both what the metadata recorded as injected and what the current guidance would\n // inject: the union covers metadata written before the record existed as well as entries left\n // behind by an older ProductShape.\n const recorded = (await readMeta(root))?.managed;\n const current = currentManagedStrings();\n const managedRules: Record<string, string[]> = {};\n for (const key of new Set([\n ...Object.keys(current.rules),\n ...Object.keys(recorded?.rules ?? {}),\n ])) {\n managedRules[key] = [\n ...new Set([...(current.rules[key] ?? []), ...(recorded?.rules[key] ?? [])]),\n ];\n }\n const managedOperations: Record<string, string[]> = {};\n for (const key of new Set([\n ...Object.keys(current.operations),\n ...Object.keys(recorded?.operations ?? {}),\n ])) {\n managedOperations[key] = [\n ...new Set([...(current.operations[key] ?? []), ...(recorded?.operations[key] ?? [])]),\n ];\n }\n\n const config = await readOpenSpecConfig(root);\n if (config !== null) {\n let configChanged = false;\n\n // Remove PDaC context block.\n const context =\n typeof config['context'] === 'string' ? (config['context'] as string) : undefined;\n if (context) {\n const pdacBlock = extractPdacContext(context);\n if (pdacBlock !== null) {\n const cleaned = context\n .replace(pdacBlock, '')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trimEnd();\n if (cleaned === '') {\n delete config['context'];\n } else {\n config['context'] = cleaned;\n }\n configChanged = true;\n }\n }\n\n // Remove PDaC rules from each artifact.\n const rules =\n config['rules'] && typeof config['rules'] === 'object' && !Array.isArray(config['rules'])\n ? (config['rules'] as Record<string, unknown>)\n : null;\n if (rules) {\n let rulesChanged = false;\n for (const [artifact, pdacRules] of Object.entries(managedRules)) {\n if (!Array.isArray(rules[artifact])) continue;\n const list = rules[artifact] as string[];\n const filtered = list.filter((r) => !pdacRules.includes(r));\n if (filtered.length !== list.length) {\n if (filtered.length === 0) {\n delete rules[artifact];\n } else {\n rules[artifact] = filtered;\n }\n rulesChanged = true;\n }\n }\n if (rulesChanged) {\n if (Object.keys(rules).length === 0) {\n delete config['rules'];\n }\n configChanged = true;\n }\n }\n\n // Remove PDaC operation guidance.\n const operations =\n config['operations'] &&\n typeof config['operations'] === 'object' &&\n !Array.isArray(config['operations'])\n ? (config['operations'] as Record<string, unknown>)\n : null;\n if (operations) {\n let opsChanged = false;\n for (const opKey of ['apply', 'archive'] as const) {\n const pdacGuidance = managedOperations[opKey] ?? [];\n const op =\n operations[opKey] &&\n typeof operations[opKey] === 'object' &&\n !Array.isArray(operations[opKey])\n ? (operations[opKey] as Record<string, unknown>)\n : null;\n if (!op || !Array.isArray(op['guidance'])) continue;\n const list = op['guidance'] as string[];\n const filtered = list.filter((g) => !pdacGuidance.includes(g));\n if (filtered.length !== list.length) {\n if (filtered.length === 0) {\n delete op['guidance'];\n } else {\n op['guidance'] = filtered;\n }\n opsChanged = true;\n }\n if (Object.keys(op).length === 0) {\n delete operations[opKey];\n }\n }\n if (opsChanged) {\n if (Object.keys(operations).length === 0) {\n delete config['operations'];\n }\n configChanged = true;\n }\n }\n\n if (configChanged && !dryRun) {\n const serialized = serializeConfig(config);\n await writeFile(configPath(root), serialized, 'utf8');\n removed.push(CONFIG_RELATIVE);\n } else if (configChanged && dryRun) {\n removed.push(CONFIG_RELATIVE);\n }\n }\n\n // Remove the installed CI example.\n if (await pathExists(ciExamplePath(root))) {\n if (!dryRun) {\n await rm(ciExamplePath(root), { force: true });\n }\n removed.push(CI_EXAMPLE_RELATIVE);\n }\n\n // Remove metadata file.\n if (await pathExists(metaPath(root))) {\n if (!dryRun) {\n await rm(metaPath(root), { force: true });\n }\n removed.push(META_RELATIVE);\n }\n\n return { removed };\n}\n","import crossSpawn from 'cross-spawn';\n\nexport interface CommandResult {\n stdout: string;\n stderr: string;\n}\n\nexport interface RunCommandOptions {\n cwd?: string;\n env?: NodeJS.ProcessEnv;\n maxBuffer?: number;\n}\n\nfunction renderCommand(command: string, args: string[]): string {\n return [command, ...args]\n .map((value) => (/^[A-Za-z0-9_./:@=+-]+$/.test(value) ? value : JSON.stringify(value)))\n .join(' ');\n}\n\n/**\n * Run one executable with an argument array and no shell interpolation.\n *\n * cross-spawn resolves Windows command shims and escapes their arguments while preserving native\n * executable spawning on POSIX. This keeps repository-local node_modules/.bin commands portable\n * without passing an argument array through `shell: true`.\n */\nexport function runCommand(\n command: string,\n args: string[],\n options: RunCommandOptions = {},\n): Promise<CommandResult> {\n return new Promise((resolve, reject) => {\n const maxBuffer = options.maxBuffer ?? 10 * 1024 * 1024;\n const child = crossSpawn(command, args, {\n cwd: options.cwd,\n env: options.env,\n stdio: ['ignore', 'pipe', 'pipe'],\n windowsHide: true,\n });\n let stdout = '';\n let stderr = '';\n let settled = false;\n\n const fail = (error: Error): void => {\n if (settled) return;\n settled = true;\n reject(error);\n };\n const append = (stream: 'stdout' | 'stderr', chunk: string): void => {\n if (settled) return;\n if (stream === 'stdout') stdout += chunk;\n else stderr += chunk;\n if (Buffer.byteLength(stdout) + Buffer.byteLength(stderr) > maxBuffer) {\n child.kill();\n fail(\n new Error(`${renderCommand(command, args)} exceeded its ${maxBuffer}-byte output limit`),\n );\n }\n };\n\n child.stdout?.setEncoding('utf8');\n child.stderr?.setEncoding('utf8');\n child.stdout?.on('data', (chunk: string) => append('stdout', chunk));\n child.stderr?.on('data', (chunk: string) => append('stderr', chunk));\n child.once('error', (error) => {\n fail(new Error(`Could not start ${renderCommand(command, args)}: ${error.message}`));\n });\n child.once('close', (code, signal) => {\n if (settled) return;\n if (code === 0) {\n settled = true;\n resolve({ stdout, stderr });\n return;\n }\n const status = code === null ? `signal ${signal ?? 'unknown'}` : `exit ${code}`;\n const detail = stderr.trim() || stdout.trim();\n fail(\n new Error(\n `${renderCommand(command, args)} failed with ${status}${detail ? `: ${detail}` : ''}`,\n ),\n );\n });\n });\n}\n","/**\n * Shared workspace helpers for the OpenSpec integration: workspace detection and the\n * child-process environment that lets a devDependency-installed OpenSpec CLI count as installed.\n */\nimport { access } from 'node:fs/promises';\nimport { join } from 'node:path';\n\n/** Check whether a filesystem path exists. */\nexport async function pathExists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n\n/** Check if an OpenSpec workspace exists at the given root (openspec/ directory present). */\nexport async function isOpenSpecWorkspace(root: string): Promise<boolean> {\n return pathExists(join(root, 'openspec'));\n}\n\n/**\n * Build a child-process environment whose PATH starts with the repository's node_modules/.bin,\n * so an OpenSpec installed as a devDependency counts as installed. Windows spells the variable\n * `Path` (and env objects lose the case-insensitive lookup once spread), so every casing is\n * collapsed into a single `PATH` entry.\n */\nexport function envWithLocalBin(root: string): NodeJS.ProcessEnv {\n const env: NodeJS.ProcessEnv = { ...process.env };\n const separator = process.platform === 'win32' ? ';' : ':';\n let existing = '';\n for (const key of Object.keys(env)) {\n if (key.toUpperCase() === 'PATH') {\n existing = env[key] ?? '';\n delete env[key];\n }\n }\n const localBin = join(root, 'node_modules', '.bin');\n env['PATH'] = existing ? `${localBin}${separator}${existing}` : localBin;\n return env;\n}\n","/**\n * OpenSpec consumer-document enumeration: the OpenSpec implementation of the framework-neutral\n * SDD integration-provider contract defined in @prodshape/core.\n *\n * Everything OpenSpec-specific about verification lives here — the `openspec/` layout, the\n * changes/archive lifecycle split, the artifact-file conventions, and the `openspec` CLI — so\n * the core package stays SDD-framework blind. The provider only enumerates the expected current\n * consumer-document population; scope classification and citation verification are core\n * responsibilities that operate on the enumeration.\n */\nimport { readdir, readFile } from 'node:fs/promises';\nimport { join, relative, sep } from 'node:path';\nimport fg from 'fast-glob';\nimport { codes } from '@prodshape/core';\nimport type {\n ConsumerDocument,\n ConsumerDocumentEnumeration,\n Diagnostic,\n EnumerateConsumerDocumentsOptions,\n SddIntegrationProvider,\n} from '@prodshape/core';\nimport { runCommand } from './process.js';\nimport { envWithLocalBin, isOpenSpecWorkspace, pathExists } from './workspace.js';\n\n/**\n * The artifact files that make up an OpenSpec change, relative to the change directory.\n * `specs/` is a directory containing one or more `<capability>/spec.md` files.\n */\nconst CHANGE_ARTIFACT_FILES: Array<{ file: string; kind: string }> = [\n { file: 'proposal.md', kind: 'proposal' },\n { file: 'design.md', kind: 'design' },\n { file: 'tasks.md', kind: 'tasks' },\n];\n\n/** Run `openspec` with the given args at the repository root, returning stdout. */\nasync function runOpenSpec(\n repoRoot: string,\n args: string[],\n): Promise<{ stdout: string; stderr: string }> {\n return runCommand('openspec', args, {\n cwd: repoRoot,\n env: envWithLocalBin(repoRoot),\n maxBuffer: 10 * 1024 * 1024,\n });\n}\n\n/** Parsed entry from `openspec list --json`. */\ninterface OpenSpecListEntry {\n name: string;\n}\n\n/**\n * Call `openspec list --json` and return the change names. Returns `null` when the CLI is\n * unavailable or the output is unparseable, so the caller can fall back to filesystem scanning.\n */\nasync function openspecListChanges(repoRoot: string): Promise<string[] | null> {\n try {\n const { stdout } = await runOpenSpec(repoRoot, ['list', '--json']);\n const parsed = JSON.parse(stdout) as { changes?: OpenSpecListEntry[] };\n if (!Array.isArray(parsed.changes)) return null;\n return parsed.changes.map((c) => c.name).filter((n): n is string => typeof n === 'string');\n } catch {\n return null;\n }\n}\n\n/** Check whether `openspec` is runnable (PATH or node_modules/.bin) with a parseable version. */\nexport async function isOpenSpecCliAvailable(repoRoot?: string): Promise<boolean> {\n try {\n const { stdout } = await runCommand('openspec', ['--version'], {\n ...(repoRoot ? { cwd: repoRoot, env: envWithLocalBin(repoRoot) } : {}),\n });\n return /\\d+\\.\\d+\\.\\d+/.test(stdout);\n } catch {\n return false;\n }\n}\n\n/** Discover all `.md` files under a directory, returning absolute paths sorted. */\nasync function discoverMarkdownFiles(dir: string): Promise<string[]> {\n const entries = await fg('**/*.md', {\n cwd: dir,\n absolute: true,\n dot: false,\n ignore: ['**/node_modules/**'],\n });\n return entries.map((e) => e.split('/').join(sep)).sort();\n}\n\n/** Build a {@link ConsumerDocument} from an absolute path. */\nfunction buildConsumerDocument(\n absolutePath: string,\n repoRoot: string,\n opts: { change?: string; archived: boolean; artifactKind?: string },\n): ConsumerDocument {\n return {\n path: relative(repoRoot, absolutePath).split(sep).join('/'),\n absolutePath,\n change: opts.change,\n archived: opts.archived,\n artifactKind: opts.artifactKind,\n };\n}\n\n/** Discover artifact files for a single change directory. */\nasync function discoverChangeDocuments(\n changeDir: string,\n repoRoot: string,\n change: string,\n archived: boolean,\n): Promise<ConsumerDocument[]> {\n const docs: ConsumerDocument[] = [];\n\n for (const { file, kind } of CHANGE_ARTIFACT_FILES) {\n const absolutePath = join(changeDir, file);\n try {\n await readFile(absolutePath);\n docs.push(\n buildConsumerDocument(absolutePath, repoRoot, { change, archived, artifactKind: kind }),\n );\n } catch {\n // File doesn't exist; skip.\n }\n }\n\n const specsDir = join(changeDir, 'specs');\n try {\n const specFiles = await discoverMarkdownFiles(specsDir);\n for (const absolutePath of specFiles) {\n docs.push(\n buildConsumerDocument(absolutePath, repoRoot, { change, archived, artifactKind: 'specs' }),\n );\n }\n } catch {\n // No specs/ directory; skip.\n }\n\n return docs;\n}\n\n/** Discover all `.md` files under `openspec/specs/` (the current specs, not changes). */\nasync function discoverSpecDocuments(\n specsDir: string,\n repoRoot: string,\n): Promise<ConsumerDocument[]> {\n const docs: ConsumerDocument[] = [];\n try {\n const specFiles = await discoverMarkdownFiles(specsDir);\n for (const absolutePath of specFiles) {\n docs.push(\n buildConsumerDocument(absolutePath, repoRoot, { archived: false, artifactKind: 'spec' }),\n );\n }\n } catch {\n // No specs/ directory; skip.\n }\n return docs;\n}\n\n/** Discover archived change directories under `openspec/changes/archive/`. */\nasync function discoverArchivedChanges(archiveDir: string): Promise<string[]> {\n try {\n const entries = await readdir(archiveDir, { withFileTypes: true });\n return entries\n .filter((e) => e.isDirectory())\n .map((e) => e.name)\n .sort();\n } catch {\n return [];\n }\n}\n\n/**\n * Enumerate the consumer-document population of an OpenSpec workspace.\n *\n * Uses `openspec list --json` to determine the current (non-archived) changes. The OpenSpec CLI\n * excludes `openspec/changes/archive/` from `list`, so the default population is current-only.\n * When `includeArchived` is true, archived changes under `openspec/changes/archive/` are also\n * scanned via the filesystem (the CLI cannot address archived changes by name).\n *\n * For each change, the artifact files are resolved by filesystem convention: `proposal.md`,\n * `specs/` (one or more spec files), `design.md`, `tasks.md`. The `openspec show --json` output\n * does not include artifact paths, so convention-based resolution is the stable contract.\n *\n * For `openspec/specs/` (the current specs, not changes), all `.md` files are discovered.\n *\n * If the OpenSpec CLI is not runnable, enumeration falls back to filesystem scanning of\n * `openspec/changes/` (excluding `archive/` unless `includeArchived`) and reports PRODUCT069:\n * the population then rests on layout convention alone, which the gate must surface rather than\n * silently trust.\n */\nexport async function enumerateOpenSpecDocuments(\n repoRoot: string,\n options?: EnumerateConsumerDocumentsOptions,\n): Promise<ConsumerDocumentEnumeration> {\n const includeArchived = options?.includeArchived ?? false;\n const root = join(repoRoot, 'openspec');\n const changesDir = join(root, 'changes');\n const archiveDir = join(changesDir, 'archive');\n const specsDir = join(root, 'specs');\n\n const documents: ConsumerDocument[] = [];\n const diagnostics: Diagnostic[] = [];\n\n // A workspace with no `openspec/changes/` directory at all (specs only) has zero changes by\n // construction: nothing to discover, no CLI needed, and no PRODUCT069 — older OpenSpec CLIs\n // (1.3.x) even reject `list` outright in such a workspace. Otherwise prefer\n // `openspec list --json` and fall back to filesystem scanning.\n let currentChangeNames: string[] | null = (await pathExists(changesDir))\n ? await openspecListChanges(repoRoot)\n : [];\n\n if (currentChangeNames === null) {\n diagnostics.push({\n severity: 'error',\n code: codes.openSpecCliMissing,\n message: `OpenSpec CLI not available; the document population was enumerated by filesystem convention instead of 'openspec list'. Install with 'npm install -g @fission-ai/openspec@1' for authoritative population discovery.`,\n file: 'openspec/',\n });\n // archive/ is excluded here: archived changes are enumerated separately below.\n try {\n const entries = await readdir(changesDir, { withFileTypes: true });\n currentChangeNames = entries\n .filter((e) => e.isDirectory() && e.name !== 'archive')\n .map((e) => e.name)\n .sort();\n } catch {\n currentChangeNames = [];\n }\n }\n\n if (options?.change) {\n const wanted = options.change;\n if (currentChangeNames.includes(wanted)) {\n currentChangeNames = [wanted];\n } else {\n // The requested change isn't in the current list. Check the archive below if asked.\n currentChangeNames = [];\n }\n }\n\n for (const name of currentChangeNames) {\n const changeDir = join(changesDir, name);\n documents.push(...(await discoverChangeDocuments(changeDir, repoRoot, name, false)));\n }\n\n if (includeArchived) {\n const archivedNames = await discoverArchivedChanges(archiveDir);\n for (const name of archivedNames) {\n // If a specific change was requested and it wasn't current, check the archive.\n if (options?.change && name !== options.change) continue;\n const changeDir = join(archiveDir, name);\n documents.push(...(await discoverChangeDocuments(changeDir, repoRoot, name, true)));\n }\n }\n\n documents.push(...(await discoverSpecDocuments(specsDir, repoRoot)));\n\n return {\n documents,\n root: 'openspec',\n includeArchived,\n diagnostics,\n };\n}\n\n/**\n * The OpenSpec implementation of the SDD integration-provider contract.\n * `prodshape citations verify --provider openspec` verifies the population this enumerates.\n */\nexport const openSpecProvider: SddIntegrationProvider = {\n name: 'openspec',\n detectWorkspace: isOpenSpecWorkspace,\n enumerateDocuments: enumerateOpenSpecDocuments,\n};\n"],"mappings":"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","names":["readFile","join","join","join","join","command","readFile"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prodshape/integration-openspec",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "OpenSpec integration for Product Definition as Code: configures OpenSpec workspaces with PDaC citation rules and verifies citations against the OpenSpec population.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -24,11 +24,13 @@
|
|
|
24
24
|
"src"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
+
"cross-spawn": "^7.0.6",
|
|
27
28
|
"fast-glob": "^3.3.0",
|
|
28
29
|
"yaml": "^2.5.0",
|
|
29
|
-
"@prodshape/core": "0.
|
|
30
|
+
"@prodshape/core": "0.16.1"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
33
|
+
"@types/cross-spawn": "^6.0.6",
|
|
32
34
|
"tsup": "^8.0.0"
|
|
33
35
|
},
|
|
34
36
|
"scripts": {
|
package/src/index.ts
CHANGED
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
* (schema, context, rules, operations, githubCopilot, and anything else) and only appends PDaC
|
|
13
13
|
* guidance, deduplicating identical entries so the operation is idempotent.
|
|
14
14
|
*/
|
|
15
|
-
import { execFile } from 'node:child_process';
|
|
16
15
|
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
17
16
|
import { dirname, join } from 'node:path';
|
|
18
17
|
import { parse, stringify } from 'yaml';
|
|
18
|
+
import { runCommand } from './process.js';
|
|
19
19
|
import { envWithLocalBin, isOpenSpecWorkspace, pathExists } from './workspace.js';
|
|
20
20
|
|
|
21
21
|
export { isOpenSpecWorkspace } from './workspace.js';
|
|
@@ -44,22 +44,25 @@ const PDAC_CONTEXT_END = '<!-- pdac:context end -->';
|
|
|
44
44
|
|
|
45
45
|
/** The PDaC authority context block injected into openspec/config.yaml `context:`. */
|
|
46
46
|
export const PDAC_CONTEXT_BLOCK = `${PDAC_CONTEXT_BEGIN}
|
|
47
|
-
This repository uses Product Definition as Code (PDaC). The canonical product definition lives in docs/product/model (actors, journeys, use cases, business rules, domain terms, requirements)
|
|
48
|
-
|
|
47
|
+
This repository uses Product Definition as Code (PDaC). The canonical product definition lives in docs/product/model (actors, journeys, use cases, business rules, domain terms, requirements). OpenSpec artifacts consume it and are never a second source of truth.
|
|
48
|
+
A citation binds consumer text to a canonical artifact: it records the artifact id, a sha256 content digest and an optional verification scenario anchor. Verification reports one status per citation: current, stale, tampered or unresolved.
|
|
49
49
|
To cite: run \`npx prodshape inspect <ID>\` to read the current digest, then \`npx prodshape cite --id <ID> --digest <digest>\` to emit the citation record. Place inline citations on their own line directly under the text they ground.
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
To find which artifacts a change impacts, compare the change's intent with the whole product definition first, then widen the result with \`npx prodshape impact <ID>\`. When the intent contradicts or goes beyond the definition, record that drift in the proposal with the marker \`<!-- pdac-drift ids="..." summary="..." -->\` (listed by \`npx prodshape drift\`) and let humans decide — never fix it quietly.
|
|
51
|
+
Every current OpenSpec document (proposal, specs, design, tasks and openspec/specs/) has exactly one scope state: bound (it carries at least one citation, or declares \`pdac-scope: cited\`), exempt (a human declared \`pdac-scope: none\` because the document has no product-semantic dependency), or unclassified (neither declared — a verification failure). Binding and exemption are human declarations: never declare \`pdac-scope: none\` merely because citations are missing, and a \`pdac-scope: cited\` document with zero citations fails. Verify with \`npx prodshape citations verify --provider openspec\`: it enumerates the expected documents, so zero discovered citations is a set of failures, never a pass. The scope gate applies to current documents only; citations in archived changes are still checked, with problems reported as warnings, because archived history cannot be edited.
|
|
52
|
+
The accepted Product Definition changes only through a Product Change under docs/product/changes/: validate it as an overlay while the baseline stays untouched, obtain human product approval, apply explicitly on a working branch, and let a human accept the resulting baseline by merging its pull request. A Product Change is not a pull request; apply is not acceptance; neither apply nor merge attests implementation, verification, release or deployment. OpenSpec changes never edit docs/product/model directly: an OpenSpec change may share a pull request with an applied Product Change or follow later, but it never supplies Product Change status.
|
|
52
53
|
${PDAC_CONTEXT_END}`;
|
|
53
54
|
|
|
54
55
|
/** The PDaC citation rules injected per artifact into openspec/config.yaml `rules:`. */
|
|
55
56
|
export const PDAC_RULES: Record<string, string[]> = {
|
|
56
57
|
proposal: [
|
|
57
58
|
'State which PDaC artifacts this change touches, each with an inline citation emitted by `npx prodshape cite --id <ID> --digest <digest>`. Never write a citation record by hand.',
|
|
59
|
+
'Find the impacted artifacts as a separate step, before writing any proposal content. Read the whole product definition (docs/product/model by default), compare it with what this change wants — the backlog item behind it, if there is one — and list every artifact the change depends on, alters or contradicts. Then widen the list: run `npx prodshape impact <ID>` on each artifact you found and check its direct and indirect neighbours. Put the resulting list in the proposal, cite every impacted artifact from each document that uses it (proposal, specs, design, tasks), and name any neighbour you checked and left out.',
|
|
60
|
+
'If this change\'s goals contradict the product definition, or need behaviour it does not describe, that is product-definition drift. Record it in the proposal under a \'Product definition drift\' note naming the artifacts involved, with the marker `<!-- pdac-drift ids="<ID>[, <ID>...]" summary="<one line>" -->` on its own line so `npx prodshape drift` can list it. The decision is human: the developer and the product owner agree whether to propose a Product Change or adjust this change. Never fix drift quietly, drop or weaken a citation to hide it, or write around the conflict.',
|
|
58
61
|
'If the change implements altered product behaviour, name the Product Change (CHG id) whose applied or accepted artifacts it implements. If no overlay-validated and human-approved Product Change exists yet, stop and ask for one instead of proceeding. The OpenSpec change is not the Product Change.',
|
|
59
62
|
'Every document of this change must end up bound or exempt: bind by citing the canonical text it depends on, or declare `pdac-scope: none` on a line of its own (frontmatter or `<!-- pdac-scope: none -->`) when a human judges the document has no product-semantic dependency. Never declare an exemption just because citations are missing.',
|
|
60
63
|
],
|
|
61
64
|
specs: [
|
|
62
|
-
'Every requirement derived from canonical product text MUST carry a citation to every PDaC artifact it derives from
|
|
65
|
+
'Every requirement derived from canonical product text MUST carry a citation to every PDaC artifact it derives from — not only the closest one — one citation per line, placed after the requirement text and before the first scenario. Never place citations between the requirement heading and the requirement text; OpenSpec reads the first paragraph as the requirement.',
|
|
63
66
|
'Cite a specific verification scenario with an anchor when the requirement maps to one (anchor="<scenario id>").',
|
|
64
67
|
'Never paraphrase canonical text as if it were new; either cite it inline or embed it in a marker-block citation (<!-- pdac:cite ... --> canonical text <!-- /pdac:cite -->).',
|
|
65
68
|
'Never invent artifact ids or digests. Read them with `npx prodshape inspect <ID>`.',
|
|
@@ -173,6 +176,10 @@ export function renderCiExample(options: { warningsAsErrors: boolean }): string
|
|
|
173
176
|
'# - unresolved citations, tampered embedded projections, unclassified current documents',
|
|
174
177
|
'# and bound documents with zero citations always fail the gate;',
|
|
175
178
|
...stalePolicy,
|
|
179
|
+
'# - citations in archived changes are checked too, but their problems are reported as',
|
|
180
|
+
'# warnings: archived history cannot be edited, so a problem there is information,',
|
|
181
|
+
'# not something this gate can make anyone fix. Pass --include-archived to apply the',
|
|
182
|
+
'# full gate to archived documents as well.',
|
|
176
183
|
'name: pdac-citations',
|
|
177
184
|
'on:',
|
|
178
185
|
' pull_request:',
|
|
@@ -222,23 +229,12 @@ function compareVersions(a: string, b: string): number {
|
|
|
222
229
|
}
|
|
223
230
|
|
|
224
231
|
/** Run a command at the repository root, resolving stdout+stderr or rejecting on failure. */
|
|
225
|
-
function runAtRoot(root: string, command: string, args: string[]): Promise<string> {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
execFile(
|
|
230
|
-
command,
|
|
231
|
-
args,
|
|
232
|
-
{ cwd: root, shell: true, env: envWithLocalBin(root) },
|
|
233
|
-
(error, stdout, stderr) => {
|
|
234
|
-
if (error) {
|
|
235
|
-
reject(new Error(`${command} ${args.join(' ')} failed: ${error.message}`));
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
resolve(`${stdout}${stderr}`);
|
|
239
|
-
},
|
|
240
|
-
);
|
|
232
|
+
async function runAtRoot(root: string, command: string, args: string[]): Promise<string> {
|
|
233
|
+
const { stdout, stderr } = await runCommand(command, args, {
|
|
234
|
+
cwd: root,
|
|
235
|
+
env: envWithLocalBin(root),
|
|
241
236
|
});
|
|
237
|
+
return `${stdout}${stderr}`;
|
|
242
238
|
}
|
|
243
239
|
|
|
244
240
|
/**
|
|
@@ -248,31 +244,23 @@ function runAtRoot(root: string, command: string, args: string[]): Promise<strin
|
|
|
248
244
|
export async function detectOpenSpecVersion(
|
|
249
245
|
root?: string,
|
|
250
246
|
): Promise<{ version: string } | { error: string }> {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if (!match?.[1]) {
|
|
269
|
-
resolve({ error: `Could not parse OpenSpec version from output: ${output}` });
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
resolve({ version: match[1] });
|
|
273
|
-
},
|
|
274
|
-
);
|
|
275
|
-
});
|
|
247
|
+
try {
|
|
248
|
+
const { stdout, stderr } = await runCommand('openspec', ['--version'], {
|
|
249
|
+
...(root ? { cwd: root, env: envWithLocalBin(root) } : {}),
|
|
250
|
+
});
|
|
251
|
+
const output = `${stdout}${stderr}`.trim();
|
|
252
|
+
// `openspec --version` prints something like "1.3.0" or "openspec/1.3.0 ...".
|
|
253
|
+
const match = output.match(/(\d+\.\d+\.\d+(?:-[^\s]+)?)/);
|
|
254
|
+
if (!match?.[1]) {
|
|
255
|
+
return { error: `Could not parse OpenSpec version from output: ${output}` };
|
|
256
|
+
}
|
|
257
|
+
return { version: match[1] };
|
|
258
|
+
} catch (error) {
|
|
259
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
260
|
+
return {
|
|
261
|
+
error: `OpenSpec CLI not found on PATH (run: npm install -g ${OPENSPEC_NPX_SPEC}). ${message}`,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
276
264
|
}
|
|
277
265
|
|
|
278
266
|
/**
|
package/src/population.ts
CHANGED
|
@@ -8,10 +8,8 @@
|
|
|
8
8
|
* consumer-document population; scope classification and citation verification are core
|
|
9
9
|
* responsibilities that operate on the enumeration.
|
|
10
10
|
*/
|
|
11
|
-
import { execFile } from 'node:child_process';
|
|
12
11
|
import { readdir, readFile } from 'node:fs/promises';
|
|
13
12
|
import { join, relative, sep } from 'node:path';
|
|
14
|
-
import { promisify } from 'node:util';
|
|
15
13
|
import fg from 'fast-glob';
|
|
16
14
|
import { codes } from '@prodshape/core';
|
|
17
15
|
import type {
|
|
@@ -21,9 +19,8 @@ import type {
|
|
|
21
19
|
EnumerateConsumerDocumentsOptions,
|
|
22
20
|
SddIntegrationProvider,
|
|
23
21
|
} from '@prodshape/core';
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
const execFileAsync = promisify(execFile);
|
|
22
|
+
import { runCommand } from './process.js';
|
|
23
|
+
import { envWithLocalBin, isOpenSpecWorkspace, pathExists } from './workspace.js';
|
|
27
24
|
|
|
28
25
|
/**
|
|
29
26
|
* The artifact files that make up an OpenSpec change, relative to the change directory.
|
|
@@ -40,12 +37,8 @@ async function runOpenSpec(
|
|
|
40
37
|
repoRoot: string,
|
|
41
38
|
args: string[],
|
|
42
39
|
): Promise<{ stdout: string; stderr: string }> {
|
|
43
|
-
|
|
44
|
-
// across platforms. The repository's node_modules/.bin is consulted first, so a
|
|
45
|
-
// devDependency install counts.
|
|
46
|
-
return execFileAsync('openspec', args, {
|
|
40
|
+
return runCommand('openspec', args, {
|
|
47
41
|
cwd: repoRoot,
|
|
48
|
-
shell: true,
|
|
49
42
|
env: envWithLocalBin(repoRoot),
|
|
50
43
|
maxBuffer: 10 * 1024 * 1024,
|
|
51
44
|
});
|
|
@@ -74,9 +67,8 @@ async function openspecListChanges(repoRoot: string): Promise<string[] | null> {
|
|
|
74
67
|
/** Check whether `openspec` is runnable (PATH or node_modules/.bin) with a parseable version. */
|
|
75
68
|
export async function isOpenSpecCliAvailable(repoRoot?: string): Promise<boolean> {
|
|
76
69
|
try {
|
|
77
|
-
const { stdout } = await
|
|
78
|
-
|
|
79
|
-
...(repoRoot ? { env: envWithLocalBin(repoRoot) } : {}),
|
|
70
|
+
const { stdout } = await runCommand('openspec', ['--version'], {
|
|
71
|
+
...(repoRoot ? { cwd: repoRoot, env: envWithLocalBin(repoRoot) } : {}),
|
|
80
72
|
});
|
|
81
73
|
return /\d+\.\d+\.\d+/.test(stdout);
|
|
82
74
|
} catch {
|
|
@@ -119,7 +111,6 @@ async function discoverChangeDocuments(
|
|
|
119
111
|
): Promise<ConsumerDocument[]> {
|
|
120
112
|
const docs: ConsumerDocument[] = [];
|
|
121
113
|
|
|
122
|
-
// Fixed artifact files: proposal.md, design.md, tasks.md.
|
|
123
114
|
for (const { file, kind } of CHANGE_ARTIFACT_FILES) {
|
|
124
115
|
const absolutePath = join(changeDir, file);
|
|
125
116
|
try {
|
|
@@ -132,7 +123,6 @@ async function discoverChangeDocuments(
|
|
|
132
123
|
}
|
|
133
124
|
}
|
|
134
125
|
|
|
135
|
-
// specs/ directory: one or more <capability>/spec.md files.
|
|
136
126
|
const specsDir = join(changeDir, 'specs');
|
|
137
127
|
try {
|
|
138
128
|
const specFiles = await discoverMarkdownFiles(specsDir);
|
|
@@ -212,8 +202,13 @@ export async function enumerateOpenSpecDocuments(
|
|
|
212
202
|
const documents: ConsumerDocument[] = [];
|
|
213
203
|
const diagnostics: Diagnostic[] = [];
|
|
214
204
|
|
|
215
|
-
//
|
|
216
|
-
|
|
205
|
+
// A workspace with no `openspec/changes/` directory at all (specs only) has zero changes by
|
|
206
|
+
// construction: nothing to discover, no CLI needed, and no PRODUCT069 — older OpenSpec CLIs
|
|
207
|
+
// (1.3.x) even reject `list` outright in such a workspace. Otherwise prefer
|
|
208
|
+
// `openspec list --json` and fall back to filesystem scanning.
|
|
209
|
+
let currentChangeNames: string[] | null = (await pathExists(changesDir))
|
|
210
|
+
? await openspecListChanges(repoRoot)
|
|
211
|
+
: [];
|
|
217
212
|
|
|
218
213
|
if (currentChangeNames === null) {
|
|
219
214
|
diagnostics.push({
|
|
@@ -222,8 +217,7 @@ export async function enumerateOpenSpecDocuments(
|
|
|
222
217
|
message: `OpenSpec CLI not available; the document population was enumerated by filesystem convention instead of 'openspec list'. Install with 'npm install -g @fission-ai/openspec@1' for authoritative population discovery.`,
|
|
223
218
|
file: 'openspec/',
|
|
224
219
|
});
|
|
225
|
-
//
|
|
226
|
-
// excluding archive/ (it's handled separately below).
|
|
220
|
+
// archive/ is excluded here: archived changes are enumerated separately below.
|
|
227
221
|
try {
|
|
228
222
|
const entries = await readdir(changesDir, { withFileTypes: true });
|
|
229
223
|
currentChangeNames = entries
|
|
@@ -235,7 +229,6 @@ export async function enumerateOpenSpecDocuments(
|
|
|
235
229
|
}
|
|
236
230
|
}
|
|
237
231
|
|
|
238
|
-
// 2. Filter to a single change if requested.
|
|
239
232
|
if (options?.change) {
|
|
240
233
|
const wanted = options.change;
|
|
241
234
|
if (currentChangeNames.includes(wanted)) {
|
|
@@ -246,13 +239,11 @@ export async function enumerateOpenSpecDocuments(
|
|
|
246
239
|
}
|
|
247
240
|
}
|
|
248
241
|
|
|
249
|
-
// 3. Discover documents for each current change.
|
|
250
242
|
for (const name of currentChangeNames) {
|
|
251
243
|
const changeDir = join(changesDir, name);
|
|
252
244
|
documents.push(...(await discoverChangeDocuments(changeDir, repoRoot, name, false)));
|
|
253
245
|
}
|
|
254
246
|
|
|
255
|
-
// 4. Discover archived changes if requested.
|
|
256
247
|
if (includeArchived) {
|
|
257
248
|
const archivedNames = await discoverArchivedChanges(archiveDir);
|
|
258
249
|
for (const name of archivedNames) {
|
|
@@ -263,7 +254,6 @@ export async function enumerateOpenSpecDocuments(
|
|
|
263
254
|
}
|
|
264
255
|
}
|
|
265
256
|
|
|
266
|
-
// 5. Discover current specs (openspec/specs/).
|
|
267
257
|
documents.push(...(await discoverSpecDocuments(specsDir, repoRoot)));
|
|
268
258
|
|
|
269
259
|
return {
|
package/src/process.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import crossSpawn from 'cross-spawn';
|
|
2
|
+
|
|
3
|
+
export interface CommandResult {
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface RunCommandOptions {
|
|
9
|
+
cwd?: string;
|
|
10
|
+
env?: NodeJS.ProcessEnv;
|
|
11
|
+
maxBuffer?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function renderCommand(command: string, args: string[]): string {
|
|
15
|
+
return [command, ...args]
|
|
16
|
+
.map((value) => (/^[A-Za-z0-9_./:@=+-]+$/.test(value) ? value : JSON.stringify(value)))
|
|
17
|
+
.join(' ');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Run one executable with an argument array and no shell interpolation.
|
|
22
|
+
*
|
|
23
|
+
* cross-spawn resolves Windows command shims and escapes their arguments while preserving native
|
|
24
|
+
* executable spawning on POSIX. This keeps repository-local node_modules/.bin commands portable
|
|
25
|
+
* without passing an argument array through `shell: true`.
|
|
26
|
+
*/
|
|
27
|
+
export function runCommand(
|
|
28
|
+
command: string,
|
|
29
|
+
args: string[],
|
|
30
|
+
options: RunCommandOptions = {},
|
|
31
|
+
): Promise<CommandResult> {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const maxBuffer = options.maxBuffer ?? 10 * 1024 * 1024;
|
|
34
|
+
const child = crossSpawn(command, args, {
|
|
35
|
+
cwd: options.cwd,
|
|
36
|
+
env: options.env,
|
|
37
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
38
|
+
windowsHide: true,
|
|
39
|
+
});
|
|
40
|
+
let stdout = '';
|
|
41
|
+
let stderr = '';
|
|
42
|
+
let settled = false;
|
|
43
|
+
|
|
44
|
+
const fail = (error: Error): void => {
|
|
45
|
+
if (settled) return;
|
|
46
|
+
settled = true;
|
|
47
|
+
reject(error);
|
|
48
|
+
};
|
|
49
|
+
const append = (stream: 'stdout' | 'stderr', chunk: string): void => {
|
|
50
|
+
if (settled) return;
|
|
51
|
+
if (stream === 'stdout') stdout += chunk;
|
|
52
|
+
else stderr += chunk;
|
|
53
|
+
if (Buffer.byteLength(stdout) + Buffer.byteLength(stderr) > maxBuffer) {
|
|
54
|
+
child.kill();
|
|
55
|
+
fail(
|
|
56
|
+
new Error(`${renderCommand(command, args)} exceeded its ${maxBuffer}-byte output limit`),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
child.stdout?.setEncoding('utf8');
|
|
62
|
+
child.stderr?.setEncoding('utf8');
|
|
63
|
+
child.stdout?.on('data', (chunk: string) => append('stdout', chunk));
|
|
64
|
+
child.stderr?.on('data', (chunk: string) => append('stderr', chunk));
|
|
65
|
+
child.once('error', (error) => {
|
|
66
|
+
fail(new Error(`Could not start ${renderCommand(command, args)}: ${error.message}`));
|
|
67
|
+
});
|
|
68
|
+
child.once('close', (code, signal) => {
|
|
69
|
+
if (settled) return;
|
|
70
|
+
if (code === 0) {
|
|
71
|
+
settled = true;
|
|
72
|
+
resolve({ stdout, stderr });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const status = code === null ? `signal ${signal ?? 'unknown'}` : `exit ${code}`;
|
|
76
|
+
const detail = stderr.trim() || stdout.trim();
|
|
77
|
+
fail(
|
|
78
|
+
new Error(
|
|
79
|
+
`${renderCommand(command, args)} failed with ${status}${detail ? `: ${detail}` : ''}`,
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|