@prodshape/integration-openspec 0.4.0 → 0.5.1

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 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) 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.\nFinding which artifacts a change impacts is understanding work: 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 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 \u2014 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 document population, so zero discovered citations is a set of failures, never a pass. The scope gate applies to current documents; citations in archived changes are still checked, but their problems are reported as warnings, because archived history cannot be edited and its drift is information for the reader.\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 -->";
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 canonical payload. Wrap it in the document's native comment (`<!-- ... -->` in Markdown) on its own line directly under the text it grounds.\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/) carries exactly one explicit scope declaration: `pdac-scope: cited` for a bound document that carries at least one citation, or `pdac-scope: none` with a non-empty human-authored reason (`pdac-scope-reason: <why>` in frontmatter, or `<!-- pdac-scope: none reason=\"<why>\" -->`) for an exempt one. Citations alone never bind: a document without a declaration is unclassified and fails verification, a bound document with zero citations fails, and an exemption without a reason or contradicted by citations fails. Binding and exemption are human declarations: never declare an exemption merely because citations are missing. Verify with `npx prodshape citations verify --provider openspec`: it enumerates the expected current documents, so zero discovered citations is a set of failures, never a pass. Archived changes are excluded by default; `--include-archived` also verifies the citations history carries, 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,19 @@ function envWithLocalBin(root) {
34
91
  }
35
92
 
36
93
  // src/population.ts
37
- import { execFile } from "child_process";
94
+ import { readFileSync } from "fs";
38
95
  import { readdir, readFile } from "fs/promises";
39
96
  import { join as join2, relative, sep } from "path";
40
- import { promisify } from "util";
41
97
  import fg from "fast-glob";
42
98
  import { codes } from "@prodshape/core";
43
- var execFileAsync = promisify(execFile);
44
99
  var CHANGE_ARTIFACT_FILES = [
45
100
  { file: "proposal.md", kind: "proposal" },
46
101
  { file: "design.md", kind: "design" },
47
102
  { file: "tasks.md", kind: "tasks" }
48
103
  ];
49
104
  async function runOpenSpec(repoRoot, args) {
50
- return execFileAsync("openspec", args, {
105
+ return runCommand("openspec", args, {
51
106
  cwd: repoRoot,
52
- shell: true,
53
107
  env: envWithLocalBin(repoRoot),
54
108
  maxBuffer: 10 * 1024 * 1024
55
109
  });
@@ -66,9 +120,8 @@ async function openspecListChanges(repoRoot) {
66
120
  }
67
121
  async function isOpenSpecCliAvailable(repoRoot) {
68
122
  try {
69
- const { stdout } = await execFileAsync("openspec", ["--version"], {
70
- shell: true,
71
- ...repoRoot ? { env: envWithLocalBin(repoRoot) } : {}
123
+ const { stdout } = await runCommand("openspec", ["--version"], {
124
+ ...repoRoot ? { cwd: repoRoot, env: envWithLocalBin(repoRoot) } : {}
72
125
  });
73
126
  return /\d+\.\d+\.\d+/.test(stdout);
74
127
  } catch {
@@ -189,8 +242,10 @@ async function enumerateOpenSpecDocuments(repoRoot, options) {
189
242
  diagnostics
190
243
  };
191
244
  }
245
+ var integrationVersion = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
192
246
  var openSpecProvider = {
193
247
  name: "openspec",
248
+ version: integrationVersion,
194
249
  detectWorkspace: isOpenSpecWorkspace,
195
250
  enumerateDocuments: enumerateOpenSpecDocuments
196
251
  };
@@ -201,23 +256,23 @@ var OPENSPEC_NPX_SPEC = "@fission-ai/openspec@1";
201
256
  var PDAC_CONTEXT_BEGIN = "<!-- pdac:context begin -->";
202
257
  var PDAC_CONTEXT_END = "<!-- pdac:context end -->";
203
258
  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) and is the single source of truth for product behaviour. OpenSpec artifacts are downstream consumers of it, never a second source of truth.
205
- Consumers 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.
206
- 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
- Finding which artifacts a change impacts is understanding work: 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.
208
- Every 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 \u2014 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 document population, so zero discovered citations is a set of failures, never a pass. The scope gate applies to current documents; citations in archived changes are still checked, but their problems are reported as warnings, because archived history cannot be edited and its drift is information for the reader.
209
- The 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.
259
+ 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.
260
+ 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.
261
+ To cite: run \`npx prodshape inspect <ID>\` to read the current digest, then \`npx prodshape cite --id <ID> --digest <digest>\` to emit the canonical payload. Wrap it in the document's native comment (\`<!-- ... -->\` in Markdown) on its own line directly under the text it grounds.
262
+ 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.
263
+ Every current OpenSpec document (proposal, specs, design, tasks and openspec/specs/) carries exactly one explicit scope declaration: \`pdac-scope: cited\` for a bound document that carries at least one citation, or \`pdac-scope: none\` with a non-empty human-authored reason (\`pdac-scope-reason: <why>\` in frontmatter, or \`<!-- pdac-scope: none reason="<why>" -->\`) for an exempt one. Citations alone never bind: a document without a declaration is unclassified and fails verification, a bound document with zero citations fails, and an exemption without a reason or contradicted by citations fails. Binding and exemption are human declarations: never declare an exemption merely because citations are missing. Verify with \`npx prodshape citations verify --provider openspec\`: it enumerates the expected current documents, so zero discovered citations is a set of failures, never a pass. Archived changes are excluded by default; \`--include-archived\` also verifies the citations history carries, reported as warnings, because archived history cannot be edited.
264
+ 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.
210
265
  ${PDAC_CONTEXT_END}`;
211
266
  var PDAC_RULES = {
212
267
  proposal: [
213
- "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.",
214
- "Find the impacted artifacts as a separate step before writing any proposal content. This is understanding work, not lookup: 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 with the graph: 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, so nothing is dropped silently.",
268
+ "State which PDaC artifacts this change touches, each with a citation payload emitted by `npx prodshape cite --id <ID> --digest <digest>` and wrapped in the document native comment (`<!-- ... -->` in Markdown). Never write a citation record by hand.",
269
+ "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.",
215
270
  '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.',
216
271
  "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.",
217
- "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."
272
+ 'Every document of this change must end up bound or exempt, each with an explicit declaration: declare `pdac-scope: cited` on a line of its own and cite the canonical text the document depends on, or declare `pdac-scope: none` with a non-empty reason (`pdac-scope-reason: <why>` in frontmatter, or `<!-- pdac-scope: none reason="<why>" -->`) when a human judges the document has no product-semantic dependency. Citations alone never bind, and never declare an exemption just because citations are missing.'
218
273
  ],
219
274
  specs: [
220
- "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.",
275
+ "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.",
221
276
  'Cite a specific verification scenario with an anchor when the requirement maps to one (anchor="<scenario id>").',
222
277
  "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 -->).",
223
278
  "Never invent artifact ids or digests. Read them with `npx prodshape inspect <ID>`."
@@ -233,7 +288,7 @@ var PDAC_RULES = {
233
288
  var PDAC_OPERATIONS = {
234
289
  apply: {
235
290
  guidance: [
236
- "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."
291
+ "Before finishing, run `npx prodshape citations verify --provider openspec` and fix every stale, tampered or unresolved citation, every unclassified document, every bound document without citations and every exemption without a reason. Refresh a stale digest by re-running `npx prodshape inspect <ID>`; never delete a citation or declare `pdac-scope: none` to silence a diagnostic."
237
292
  ]
238
293
  },
239
294
  archive: {
@@ -323,45 +378,30 @@ function compareVersions(a, b) {
323
378
  if (aMin !== bMin) return aMin - bMin;
324
379
  return aPatch - bPatch;
325
380
  }
326
- function runAtRoot(root, command, args) {
327
- return new Promise((resolve, reject) => {
328
- execFile2(
329
- command,
330
- args,
331
- { cwd: root, shell: true, env: envWithLocalBin(root) },
332
- (error, stdout, stderr) => {
333
- if (error) {
334
- reject(new Error(`${command} ${args.join(" ")} failed: ${error.message}`));
335
- return;
336
- }
337
- resolve(`${stdout}${stderr}`);
338
- }
339
- );
381
+ async function runAtRoot(root, command, args) {
382
+ const { stdout, stderr } = await runCommand(command, args, {
383
+ cwd: root,
384
+ env: envWithLocalBin(root)
340
385
  });
386
+ return `${stdout}${stderr}`;
341
387
  }
342
388
  async function detectOpenSpecVersion(root) {
343
- return new Promise((resolve) => {
344
- execFile2(
345
- "openspec",
346
- ["--version"],
347
- { shell: true, ...root ? { env: envWithLocalBin(root) } : {} },
348
- (error, stdout, stderr) => {
349
- if (error) {
350
- resolve({
351
- error: `OpenSpec CLI not found on PATH (run: npm install -g ${OPENSPEC_NPX_SPEC}). ${error.message}`
352
- });
353
- return;
354
- }
355
- const output = `${stdout}${stderr}`.trim();
356
- const match = output.match(/(\d+\.\d+\.\d+(?:-[^\s]+)?)/);
357
- if (!match?.[1]) {
358
- resolve({ error: `Could not parse OpenSpec version from output: ${output}` });
359
- return;
360
- }
361
- resolve({ version: match[1] });
362
- }
363
- );
364
- });
389
+ try {
390
+ const { stdout, stderr } = await runCommand("openspec", ["--version"], {
391
+ ...root ? { cwd: root, env: envWithLocalBin(root) } : {}
392
+ });
393
+ const output = `${stdout}${stderr}`.trim();
394
+ const match = output.match(/(\d+\.\d+\.\d+(?:-[^\s]+)?)/);
395
+ if (!match?.[1]) {
396
+ return { error: `Could not parse OpenSpec version from output: ${output}` };
397
+ }
398
+ return { version: match[1] };
399
+ } catch (error) {
400
+ const message = error instanceof Error ? error.message : String(error);
401
+ return {
402
+ error: `OpenSpec CLI not found on PATH (run: npm install -g ${OPENSPEC_NPX_SPEC}). ${message}`
403
+ };
404
+ }
365
405
  }
366
406
  async function bootstrapOpenSpecWorkspace(root) {
367
407
  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.\nFinding which artifacts a change impacts is understanding work: 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 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 document population, so zero discovered citations is a set of failures, never a pass. The scope gate applies to current documents; citations in archived changes are still checked, but their problems are reported as warnings, because archived history cannot be edited and its drift is information for the reader.\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 'Find the impacted artifacts as a separate step before writing any proposal content. This is understanding work, not lookup: 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 with the graph: 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, so nothing is dropped silently.',\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, 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 '# - 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. */\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, pathExists } 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. A workspace with no `openspec/changes/` directory at all (specs\n // only) has zero changes by construction: nothing to discover, no CLI needed, and no PRODUCT069\n // — older OpenSpec CLIs (1.3.x) even reject `list` outright in such a workspace. Otherwise\n // prefer `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 // 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":";AAcA,SAAS,YAAAA,iBAAgB;AACzB,SAAS,OAAO,YAAAC,WAAU,IAAI,iBAAiB;AAC/C,SAAS,SAAS,QAAAC,aAAY;AAC9B,SAAS,OAAO,iBAAiB;;;ACbjC,SAAS,cAAc;AACvB,SAAS,YAAY;AAGrB,eAAsB,WAAW,MAAgC;AAC/D,MAAI;AACF,UAAM,OAAO,IAAI;AACjB,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,eAAsB,oBAAoB,MAAgC;AACxE,SAAO,WAAW,KAAK,MAAM,UAAU,CAAC;AAC1C;AAQO,SAAS,gBAAgB,MAAiC;AAC/D,QAAM,MAAyB,EAAE,GAAG,QAAQ,IAAI;AAChD,QAAM,YAAY,QAAQ,aAAa,UAAU,MAAM;AACvD,MAAI,WAAW;AACf,aAAW,OAAO,OAAO,KAAK,GAAG,GAAG;AAClC,QAAI,IAAI,YAAY,MAAM,QAAQ;AAChC,iBAAW,IAAI,GAAG,KAAK;AACvB,aAAO,IAAI,GAAG;AAAA,IAChB;AAAA,EACF;AACA,QAAM,WAAW,KAAK,MAAM,gBAAgB,MAAM;AAClD,MAAI,MAAM,IAAI,WAAW,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,KAAK;AAChE,SAAO;AACT;;;AC/BA,SAAS,gBAAgB;AACzB,SAAS,SAAS,gBAAgB;AAClC,SAAS,QAAAC,OAAM,UAAU,WAAW;AACpC,SAAS,iBAAiB;AAC1B,OAAO,QAAQ;AACf,SAAS,aAAa;AAUtB,IAAM,gBAAgB,UAAU,QAAQ;AAMxC,IAAM,wBAA+D;AAAA,EACnE,EAAE,MAAM,eAAe,MAAM,WAAW;AAAA,EACxC,EAAE,MAAM,aAAa,MAAM,SAAS;AAAA,EACpC,EAAE,MAAM,YAAY,MAAM,QAAQ;AACpC;AAGA,eAAe,YACb,UACA,MAC6C;AAI7C,SAAO,cAAc,YAAY,MAAM;AAAA,IACrC,KAAK;AAAA,IACL,OAAO;AAAA,IACP,KAAK,gBAAgB,QAAQ;AAAA,IAC7B,WAAW,KAAK,OAAO;AAAA,EACzB,CAAC;AACH;AAWA,eAAe,oBAAoB,UAA4C;AAC7E,MAAI;AACF,UAAM,EAAE,OAAO,IAAI,MAAM,YAAY,UAAU,CAAC,QAAQ,QAAQ,CAAC;AACjE,UAAM,SAAS,KAAK,MAAM,MAAM;AAChC,QAAI,CAAC,MAAM,QAAQ,OAAO,OAAO,EAAG,QAAO;AAC3C,WAAO,OAAO,QAAQ,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,MAAmB,OAAO,MAAM,QAAQ;AAAA,EAC3F,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,eAAsB,uBAAuB,UAAqC;AAChF,MAAI;AACF,UAAM,EAAE,OAAO,IAAI,MAAM,cAAc,YAAY,CAAC,WAAW,GAAG;AAAA,MAChE,OAAO;AAAA,MACP,GAAI,WAAW,EAAE,KAAK,gBAAgB,QAAQ,EAAE,IAAI,CAAC;AAAA,IACvD,CAAC;AACD,WAAO,gBAAgB,KAAK,MAAM;AAAA,EACpC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,eAAe,sBAAsB,KAAgC;AACnE,QAAM,UAAU,MAAM,GAAG,WAAW;AAAA,IAClC,KAAK;AAAA,IACL,UAAU;AAAA,IACV,KAAK;AAAA,IACL,QAAQ,CAAC,oBAAoB;AAAA,EAC/B,CAAC;AACD,SAAO,QAAQ,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,EAAE,KAAK,GAAG,CAAC,EAAE,KAAK;AACzD;AAGA,SAAS,sBACP,cACA,UACA,MACkB;AAClB,SAAO;AAAA,IACL,MAAM,SAAS,UAAU,YAAY,EAAE,MAAM,GAAG,EAAE,KAAK,GAAG;AAAA,IAC1D;AAAA,IACA,QAAQ,KAAK;AAAA,IACb,UAAU,KAAK;AAAA,IACf,cAAc,KAAK;AAAA,EACrB;AACF;AAGA,eAAe,wBACb,WACA,UACA,QACA,UAC6B;AAC7B,QAAM,OAA2B,CAAC;AAGlC,aAAW,EAAE,MAAM,KAAK,KAAK,uBAAuB;AAClD,UAAM,eAAeC,MAAK,WAAW,IAAI;AACzC,QAAI;AACF,YAAM,SAAS,YAAY;AAC3B,WAAK;AAAA,QACH,sBAAsB,cAAc,UAAU,EAAE,QAAQ,UAAU,cAAc,KAAK,CAAC;AAAA,MACxF;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAM,WAAWA,MAAK,WAAW,OAAO;AACxC,MAAI;AACF,UAAM,YAAY,MAAM,sBAAsB,QAAQ;AACtD,eAAW,gBAAgB,WAAW;AACpC,WAAK;AAAA,QACH,sBAAsB,cAAc,UAAU,EAAE,QAAQ,UAAU,cAAc,QAAQ,CAAC;AAAA,MAC3F;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,SAAO;AACT;AAGA,eAAe,sBACb,UACA,UAC6B;AAC7B,QAAM,OAA2B,CAAC;AAClC,MAAI;AACF,UAAM,YAAY,MAAM,sBAAsB,QAAQ;AACtD,eAAW,gBAAgB,WAAW;AACpC,WAAK;AAAA,QACH,sBAAsB,cAAc,UAAU,EAAE,UAAU,OAAO,cAAc,OAAO,CAAC;AAAA,MACzF;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAGA,eAAe,wBAAwB,YAAuC;AAC5E,MAAI;AACF,UAAM,UAAU,MAAM,QAAQ,YAAY,EAAE,eAAe,KAAK,CAAC;AACjE,WAAO,QACJ,OAAO,CAAC,MAAM,EAAE,YAAY,CAAC,EAC7B,IAAI,CAAC,MAAM,EAAE,IAAI,EACjB,KAAK;AAAA,EACV,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAqBA,eAAsB,2BACpB,UACA,SACsC;AACtC,QAAM,kBAAkB,SAAS,mBAAmB;AACpD,QAAM,OAAOA,MAAK,UAAU,UAAU;AACtC,QAAM,aAAaA,MAAK,MAAM,SAAS;AACvC,QAAM,aAAaA,MAAK,YAAY,SAAS;AAC7C,QAAM,WAAWA,MAAK,MAAM,OAAO;AAEnC,QAAM,YAAgC,CAAC;AACvC,QAAM,cAA4B,CAAC;AAMnC,MAAI,qBAAuC,MAAM,WAAW,UAAU,IAClE,MAAM,oBAAoB,QAAQ,IAClC,CAAC;AAEL,MAAI,uBAAuB,MAAM;AAC/B,gBAAY,KAAK;AAAA,MACf,UAAU;AAAA,MACV,MAAM,MAAM;AAAA,MACZ,SAAS;AAAA,MACT,MAAM;AAAA,IACR,CAAC;AAGD,QAAI;AACF,YAAM,UAAU,MAAM,QAAQ,YAAY,EAAE,eAAe,KAAK,CAAC;AACjE,2BAAqB,QAClB,OAAO,CAAC,MAAM,EAAE,YAAY,KAAK,EAAE,SAAS,SAAS,EACrD,IAAI,CAAC,MAAM,EAAE,IAAI,EACjB,KAAK;AAAA,IACV,QAAQ;AACN,2BAAqB,CAAC;AAAA,IACxB;AAAA,EACF;AAGA,MAAI,SAAS,QAAQ;AACnB,UAAM,SAAS,QAAQ;AACvB,QAAI,mBAAmB,SAAS,MAAM,GAAG;AACvC,2BAAqB,CAAC,MAAM;AAAA,IAC9B,OAAO;AAEL,2BAAqB,CAAC;AAAA,IACxB;AAAA,EACF;AAGA,aAAW,QAAQ,oBAAoB;AACrC,UAAM,YAAYA,MAAK,YAAY,IAAI;AACvC,cAAU,KAAK,GAAI,MAAM,wBAAwB,WAAW,UAAU,MAAM,KAAK,CAAE;AAAA,EACrF;AAGA,MAAI,iBAAiB;AACnB,UAAM,gBAAgB,MAAM,wBAAwB,UAAU;AAC9D,eAAW,QAAQ,eAAe;AAEhC,UAAI,SAAS,UAAU,SAAS,QAAQ,OAAQ;AAChD,YAAM,YAAYA,MAAK,YAAY,IAAI;AACvC,gBAAU,KAAK,GAAI,MAAM,wBAAwB,WAAW,UAAU,MAAM,IAAI,CAAE;AAAA,IACpF;AAAA,EACF;AAGA,YAAU,KAAK,GAAI,MAAM,sBAAsB,UAAU,QAAQ,CAAE;AAEnE,SAAO;AAAA,IACL;AAAA,IACA,MAAM;AAAA,IACN;AAAA,IACA;AAAA,EACF;AACF;AAMO,IAAM,mBAA2C;AAAA,EACtD,MAAM;AAAA,EACN,iBAAiB;AAAA,EACjB,oBAAoB;AACtB;;;AFrQO,IAAM,yBAAyB;AAO/B,IAAM,oBAAoB;AAMjC,IAAM,qBAAqB;AAC3B,IAAM,mBAAmB;AAGlB,IAAM,qBAAqB,GAAG,kBAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOrD,gBAAgB;AAGX,IAAM,aAAuC;AAAA,EAClD,UAAU;AAAA,IACR;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA,OAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA,QAAQ;AAAA,IACN;AAAA,EACF;AAAA,EACA,OAAO;AAAA,IACL;AAAA,IACA;AAAA,EACF;AACF;AAGO,IAAM,kBAGT;AAAA,EACF,OAAO;AAAA,IACL,UAAU;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAAA,EACA,SAAS;AAAA,IACP,UAAU;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACF;AAaO,SAAS,wBAAwC;AACtD,SAAO;AAAA,IACL,OAAO,OAAO,YAAY,OAAO,QAAQ,UAAU,EAAE,IAAI,CAAC,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;AAAA,IAC3F,YAAY,OAAO;AAAA,MACjB,OAAO,QAAQ,eAAe,EAAE,IAAI,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,GAAG,GAAG,QAAQ,CAAC,CAAC;AAAA,IAC5E;AAAA,EACF;AACF;AAgBA,IAAM,kBAAkB;AACxB,IAAM,gBAAgB;AACtB,IAAM,sBAAsB;AAGrB,IAAM,yBAAyB;AAG/B,IAAM,+BAA+B;AAGrC,IAAM,0BAA0B;AAYhC,SAAS,gBAAgB,SAAgD;AAC9E,QAAM,cAAc,QAAQ,mBACxB;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,EACF,IACA;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACJ,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG;AAAA,IACH;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,+BAA+B,iBAAiB;AAAA,IAChD,gBAAgB,uBAAuB;AAAA,IACvC;AAAA,EACF,EAAE,KAAK,IAAI;AACb;AAEA,SAAS,WAAW,MAAsB;AACxC,SAAOC,MAAK,MAAM,GAAG,gBAAgB,MAAM,GAAG,CAAC;AACjD;AAEA,SAAS,SAAS,MAAsB;AACtC,SAAOA,MAAK,MAAM,GAAG,cAAc,MAAM,GAAG,CAAC;AAC/C;AAEA,SAAS,cAAc,MAAsB;AAC3C,SAAOA,MAAK,MAAM,GAAG,oBAAoB,MAAM,GAAG,CAAC;AACrD;AAMA,SAAS,gBAAgB,GAAW,GAAmB;AACrD,QAAM,aAAa,CAAC,MAAwC;AAC1D,UAAM,OAAO,EAAE,MAAM,GAAG,EAAE,CAAC,GAAG,MAAM,GAAG,EAAE,CAAC,KAAK;AAC/C,UAAM,WAAW,KAAK,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,OAAO,SAAS,GAAG,EAAE,CAAC;AAClE,WAAO,CAAC,SAAS,CAAC,KAAK,GAAG,SAAS,CAAC,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC;AAAA,EAC9D;AACA,QAAM,CAAC,MAAM,MAAM,MAAM,IAAI,WAAW,CAAC;AACzC,QAAM,CAAC,MAAM,MAAM,MAAM,IAAI,WAAW,CAAC;AACzC,MAAI,SAAS,KAAM,QAAO,OAAO;AACjC,MAAI,SAAS,KAAM,QAAO,OAAO;AACjC,SAAO,SAAS;AAClB;AAGA,SAAS,UAAU,MAAc,SAAiB,MAAiC;AACjF,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AAGtC,IAAAC;AAAA,MACE;AAAA,MACA;AAAA,MACA,EAAE,KAAK,MAAM,OAAO,MAAM,KAAK,gBAAgB,IAAI,EAAE;AAAA,MACrD,CAAC,OAAO,QAAQ,WAAW;AACzB,YAAI,OAAO;AACT,iBAAO,IAAI,MAAM,GAAG,OAAO,IAAI,KAAK,KAAK,GAAG,CAAC,YAAY,MAAM,OAAO,EAAE,CAAC;AACzE;AAAA,QACF;AACA,gBAAQ,GAAG,MAAM,GAAG,MAAM,EAAE;AAAA,MAC9B;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAMA,eAAsB,sBACpB,MACkD;AAClD,SAAO,IAAI,QAAQ,CAAC,YAAY;AAG9B,IAAAA;AAAA,MACE;AAAA,MACA,CAAC,WAAW;AAAA,MACZ,EAAE,OAAO,MAAM,GAAI,OAAO,EAAE,KAAK,gBAAgB,IAAI,EAAE,IAAI,CAAC,EAAG;AAAA,MAC/D,CAAC,OAAO,QAAQ,WAAW;AACzB,YAAI,OAAO;AACT,kBAAQ;AAAA,YACN,OAAO,uDAAuD,iBAAiB,MAAM,MAAM,OAAO;AAAA,UACpG,CAAC;AACD;AAAA,QACF;AACA,cAAM,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK;AAEzC,cAAM,QAAQ,OAAO,MAAM,6BAA6B;AACxD,YAAI,CAAC,QAAQ,CAAC,GAAG;AACf,kBAAQ,EAAE,OAAO,iDAAiD,MAAM,GAAG,CAAC;AAC5E;AAAA,QACF;AACA,gBAAQ,EAAE,SAAS,MAAM,CAAC,EAAE,CAAC;AAAA,MAC/B;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAYA,eAAsB,2BACpB,MACgE;AAChE,MAAI,MAAM,oBAAoB,IAAI,GAAG;AACnC,UAAM,IAAI,MAAM,oDAAoD;AAAA,EACtE;AACA,QAAM,WAAW,MAAM,sBAAsB,IAAI;AACjD,MAAI,EAAE,WAAW,WAAW;AAC1B,QAAI,gBAAgB,SAAS,SAAS,sBAAsB,IAAI,GAAG;AACjE,YAAM,IAAI;AAAA,QACR,wBAAwB,SAAS,OAAO,mCAAmC,sBAAsB;AAAA,MACnG;AAAA,IACF;AACA,UAAMC,WAAU;AAChB,UAAM,UAAU,MAAM,YAAY,CAAC,QAAQ,WAAW,MAAM,CAAC;AAC7D,WAAO,EAAE,SAAS,SAAS,SAAS,QAAQ,OAAO,SAAAA,SAAQ;AAAA,EAC7D;AACA,QAAM,UAAU,UAAU,iBAAiB;AAC3C,QAAM,UAAU,MAAM,OAAO,CAAC,MAAM,mBAAmB,QAAQ,WAAW,MAAM,CAAC;AAEjF,QAAM,SAAS,MAAM,UAAU,MAAM,OAAO,CAAC,MAAM,mBAAmB,WAAW,CAAC;AAClF,QAAM,QAAQ,OAAO,KAAK,EAAE,MAAM,6BAA6B;AAC/D,MAAI,CAAC,QAAQ,CAAC,GAAG;AACf,UAAM,IAAI,MAAM,qDAAqD,OAAO,KAAK,CAAC,EAAE;AAAA,EACtF;AACA,SAAO,EAAE,SAAS,MAAM,CAAC,GAAG,QAAQ,MAAM,QAAQ;AACpD;AAGA,eAAsB,+BAA+B,MAAgC;AACnF,SAAO,WAAW,SAAS,IAAI,CAAC;AAClC;AAGA,eAAsB,mBAAmB,MAAuD;AAC9F,QAAM,OAAO,WAAW,IAAI;AAC5B,MAAI,CAAE,MAAM,WAAW,IAAI,EAAI,QAAO;AACtC,QAAM,OAAO,MAAMC,UAAS,MAAM,MAAM;AACxC,QAAM,SAAS,MAAM,IAAI;AACzB,MAAI,WAAW,QAAQ,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GAAG;AAE1E,WAAO,CAAC;AAAA,EACV;AACA,SAAO;AACT;AAKA,SAAS,mBAAmB,SAA4C;AACtE,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,QAAQ,QAAQ,QAAQ,kBAAkB;AAChD,QAAM,MAAM,QAAQ,QAAQ,gBAAgB;AAC5C,MAAI,UAAU,MAAM,QAAQ,MAAM,MAAM,MAAO,QAAO;AACtD,SAAO,QAAQ,MAAM,OAAO,MAAM,iBAAiB,MAAM;AAC3D;AAmBO,SAAS,YACd,UACA,mBAIA;AACA,QAAM,SAAkC,EAAE,GAAI,YAAY,CAAC,EAAG;AAC9D,QAAM,UAAoB,CAAC;AAG3B,QAAM,eAAe,CAAC,UAAgC,aACnD,YAAY,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,QAAQ,SAAS,KAAK,CAAC;AAG7D,QAAM,kBACJ,OAAO,OAAO,SAAS,MAAM,WAAY,OAAO,SAAS,IAAe;AAC1E,QAAM,oBAAoB,mBAAmB,eAAe;AAC5D,MAAI,sBAAsB,MAAM;AAE9B,QAAI,sBAAsB,oBAAoB;AAC5C,YAAM,WAAW,gBAAiB,QAAQ,mBAAmB,kBAAkB;AAC/E,UAAI,aAAa,iBAAiB;AAChC,eAAO,SAAS,IAAI;AACpB,gBAAQ,KAAK,gEAAgE;AAAA,MAC/E;AAAA,IACF;AAAA,EAEF,WAAW,iBAAiB;AAE1B,UAAM,YAAY,gBAAgB,SAAS,IAAI,IAAI,OAAO;AAC1D,WAAO,SAAS,IAAI,GAAG,eAAe,GAAG,SAAS,GAAG,kBAAkB;AACvE,YAAQ,KAAK,wEAAwE;AAAA,EACvF,OAAO;AACL,WAAO,SAAS,IAAI;AACpB,YAAQ,KAAK,8DAA8D;AAAA,EAC7E;AAGA,QAAM,gBACJ,OAAO,OAAO,KAAK,OAAO,OAAO,OAAO,MAAM,YAAY,CAAC,MAAM,QAAQ,OAAO,OAAO,CAAC,IACpF,EAAE,GAAI,OAAO,OAAO,EAA8B,IAClD,CAAC;AACP,MAAI,eAAe;AACnB,MAAI,oBAAoB;AACxB,QAAM,gBAAgB,oBAAI,IAAI;AAAA,IAC5B,GAAG,OAAO,KAAK,UAAU;AAAA,IACzB,GAAG,OAAO,KAAK,mBAAmB,SAAS,CAAC,CAAC;AAAA,EAC/C,CAAC;AACD,aAAW,YAAY,eAAe;AACpC,UAAM,YAAY,WAAW,QAAQ,KAAK,CAAC;AAC3C,UAAM,QAAQ,aAAa,mBAAmB,MAAM,QAAQ,GAAG,SAAS;AACxE,UAAM,eAAe,MAAM,QAAQ,cAAc,QAAQ,CAAC,IACtD,CAAC,GAAI,cAAc,QAAQ,CAAc,IACzC,CAAC;AACL,UAAM,SAAS,aAAa,OAAO,CAAC,UAAU,CAAC,MAAM,SAAS,KAAK,CAAC;AACpE,QAAI,OAAO,WAAW,aAAa,QAAQ;AACzC,0BAAoB;AACpB,qBAAe;AAAA,IACjB;AACA,eAAW,QAAQ,WAAW;AAC5B,UAAI,CAAC,OAAO,SAAS,IAAI,GAAG;AAC1B,eAAO,KAAK,IAAI;AAChB,uBAAe;AAAA,MACjB;AAAA,IACF;AACA,QAAI,OAAO,WAAW,GAAG;AACvB,aAAO,cAAc,QAAQ;AAAA,IAC/B,OAAO;AACL,oBAAc,QAAQ,IAAI;AAAA,IAC5B;AAAA,EACF;AACA,MAAI,cAAc;AAChB,QAAI,OAAO,KAAK,aAAa,EAAE,WAAW,GAAG;AAC3C,aAAO,OAAO,OAAO;AAAA,IACvB,OAAO;AACL,aAAO,OAAO,IAAI;AAAA,IACpB;AACA,YAAQ;AAAA,MACN,oBACI,oGACA;AAAA,IACN;AAAA,EACF;AAGA,QAAM,qBACJ,OAAO,YAAY,KACnB,OAAO,OAAO,YAAY,MAAM,YAChC,CAAC,MAAM,QAAQ,OAAO,YAAY,CAAC,IAC/B,EAAE,GAAI,OAAO,YAAY,EAA8B,IACvD,CAAC;AACP,MAAI,aAAa;AACjB,MAAI,kBAAkB;AACtB,aAAW,SAAS,CAAC,SAAS,SAAS,GAAY;AACjD,UAAM,eAAe,gBAAgB,KAAK,EAAE;AAC5C,UAAM,QAAQ,aAAa,mBAAmB,WAAW,KAAK,GAAG,YAAY;AAC7E,UAAM,aACJ,mBAAmB,KAAK,KACxB,OAAO,mBAAmB,KAAK,MAAM,YACrC,CAAC,MAAM,QAAQ,mBAAmB,KAAK,CAAC,IACpC,EAAE,GAAI,mBAAmB,KAAK,EAA8B,IAC5D,CAAC;AACP,UAAM,mBAAmB,MAAM,QAAQ,WAAW,UAAU,CAAC,IACzD,CAAC,GAAI,WAAW,UAAU,CAAc,IACxC,CAAC;AACL,UAAM,SAAS,iBAAiB,OAAO,CAAC,UAAU,CAAC,MAAM,SAAS,KAAK,CAAC;AACxE,QAAI,OAAO,WAAW,iBAAiB,QAAQ;AAC7C,wBAAkB;AAClB,mBAAa;AAAA,IACf;AACA,eAAW,YAAY,cAAc;AACnC,UAAI,CAAC,OAAO,SAAS,QAAQ,GAAG;AAC9B,eAAO,KAAK,QAAQ;AACpB,qBAAa;AAAA,MACf;AAAA,IACF;AACA,eAAW,UAAU,IAAI;AACzB,uBAAmB,KAAK,IAAI;AAAA,EAC9B;AACA,MAAI,YAAY;AACd,WAAO,YAAY,IAAI;AACvB,YAAQ;AAAA,MACN,kBACI,6GACA;AAAA,IACN;AAAA,EACF;AAEA,SAAO,EAAE,QAAQ,QAAQ;AAC3B;AAGO,SAAS,gBAAgB,QAAyC;AACvE,SAAO,GAAG,UAAU,MAAM,EAAE,QAAQ,CAAC;AAAA;AACvC;AAGA,eAAe,sBAAuC;AACpD,QAAM,cAAc,KAAK;AAAA,IACvB,MAAMA,UAAS,IAAI,IAAI,mBAAmB,YAAY,GAAG,GAAG,MAAM;AAAA,EACpE;AACA,SAAO,YAAY;AACrB;AAgBA,eAAsB,uBACpB,MACA,UASI,CAAC,GAC6E;AAClF,QAAM,EAAE,QAAQ,OAAO,SAAS,OAAO,mBAAmB,MAAM,IAAI;AAEpE,MAAI,kBAAkB,QAAQ;AAC9B,MAAI,CAAC,iBAAiB;AACpB,UAAM,WAAW,MAAM,sBAAsB,IAAI;AACjD,QAAI,WAAW,UAAU;AACvB,YAAM,IAAI,MAAM,SAAS,KAAK;AAAA,IAChC;AACA,sBAAkB,SAAS;AAAA,EAC7B;AACA,MAAI,gBAAgB,iBAAiB,sBAAsB,IAAI,GAAG;AAChE,UAAM,IAAI;AAAA,MACR,wBAAwB,eAAe,mCAAmC,sBAAsB;AAAA,IAClG;AAAA,EACF;AAEA,MAAI,CAAE,MAAM,oBAAoB,IAAI,GAAI;AACtC,UAAM,IAAI,MAAM,yDAAyD;AAAA,EAC3E;AAEA,QAAM,WAAW,MAAM,mBAAmB,IAAI;AAC9C,QAAM,eAAe,MAAM,SAAS,IAAI;AAExC,QAAM,EAAE,QAAQ,SAAS,cAAc,IAAI,YAAY,UAAU,cAAc,OAAO;AAEtF,QAAM,YAAY,gBAAgB,EAAE,iBAAiB,CAAC;AACtD,QAAM,aAAa,cAAc,IAAI;AACrC,QAAM,aAAc,MAAM,WAAW,UAAU,IAAK,MAAMA,UAAS,YAAY,MAAM,IAAI;AACzF,QAAM,YAAY,eAAe;AAEjC,QAAM,UAAU,CAAC,GAAG,aAAa;AACjC,MAAI,WAAW;AACb,YAAQ;AAAA,MACN,eAAe,OACX,8CAA8C,mBAAmB,MACjE,4CAA4C,mBAAmB;AAAA,IACrE;AAAA,EACF;AAEA,QAAM,OAAgC;AAAA,IACpC,UAAU;AAAA,IACV,SAAS,MAAM,oBAAoB;AAAA,IACnC;AAAA,IACA,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpC,YAAY;AAAA,IACZ,eAAe;AAAA,IACf,SAAS,sBAAsB;AAAA,EACjC;AAEA,MAAI,QAAQ,WAAW,KAAK,CAAC,OAAO;AAElC,QAAI,CAAC,QAAQ;AACX,YAAM,UAAU,MAAM,IAAI;AAAA,IAC5B;AACA,WAAO,EAAE,SAAS,CAAC,GAAG,SAAS,CAAC,GAAG,KAAK;AAAA,EAC1C;AAEA,QAAM,UAAoB,CAAC;AAC3B,MAAI,CAAC,QAAQ;AACX,QAAI,cAAc,SAAS,KAAK,OAAO;AACrC,YAAM,aAAa,gBAAgB,MAAM;AACzC,YAAM,MAAM,QAAQ,WAAW,IAAI,CAAC,GAAG,EAAE,WAAW,KAAK,CAAC;AAC1D,YAAM,UAAU,WAAW,IAAI,GAAG,YAAY,MAAM;AACpD,cAAQ,KAAK,eAAe;AAAA,IAC9B;AACA,QAAI,aAAa,OAAO;AACtB,YAAM,MAAM,QAAQ,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC;AACpD,YAAM,UAAU,YAAY,WAAW,MAAM;AAC7C,cAAQ,KAAK,mBAAmB;AAAA,IAClC;AACA,UAAM,UAAU,MAAM,IAAI;AAC1B,YAAQ,KAAK,aAAa;AAAA,EAC5B;AAEA,SAAO,EAAE,SAAS,SAAS,KAAK;AAClC;AAGA,eAAe,UAAU,MAAc,MAA8C;AACnF,QAAM,OAAO,SAAS,IAAI;AAC1B,QAAM,MAAM,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAC9C,QAAM,UAAU,MAAM,GAAG,KAAK,UAAU,MAAM,MAAM,CAAC,CAAC;AAAA,GAAM,MAAM;AACpE;AAGA,eAAe,SAAS,MAAuD;AAC7E,QAAM,OAAO,SAAS,IAAI;AAC1B,MAAI,CAAE,MAAM,WAAW,IAAI,EAAI,QAAO;AACtC,SAAO,KAAK,MAAM,MAAMA,UAAS,MAAM,MAAM,CAAC;AAChD;AAMA,eAAsB,0BACpB,MACA,UAA6E,CAAC,GAC3B;AAGnD,QAAM,SAAS,MAAM,uBAAuB,MAAM;AAAA,IAChD,OAAO;AAAA,IACP,QAAQ,QAAQ;AAAA,IAChB,kBAAkB,QAAQ;AAAA,EAC5B,CAAC;AACD,SAAO,EAAE,SAAS,OAAO,SAAS,SAAS,OAAO,QAAQ;AAC5D;AASA,eAAsB,yBAAyB,MAG5C;AACD,QAAM,SAA0D,CAAC;AAGjE,QAAM,WAAW,MAAM,sBAAsB,IAAI;AACjD,MAAI,WAAW,UAAU;AACvB,WAAO,KAAK,EAAE,MAAM,gBAAgB,IAAI,OAAO,QAAQ,SAAS,MAAM,CAAC;AAAA,EACzE,WAAW,gBAAgB,SAAS,SAAS,sBAAsB,IAAI,GAAG;AACxE,WAAO,KAAK;AAAA,MACV,MAAM;AAAA,MACN,IAAI;AAAA,MACJ,QAAQ,YAAY,SAAS,OAAO,+BAA+B,sBAAsB;AAAA,IAC3F,CAAC;AAAA,EACH,OAAO;AACL,WAAO,KAAK;AAAA,MACV,MAAM;AAAA,MACN,IAAI;AAAA,MACJ,QAAQ,YAAY,SAAS,OAAO;AAAA,IACtC,CAAC;AAAA,EACH;AAGA,QAAM,SAAS,MAAM,mBAAmB,IAAI;AAC5C,MAAI,WAAW,MAAM;AACnB,WAAO,KAAK;AAAA,MACV,MAAM;AAAA,MACN,IAAI;AAAA,MACJ,QAAQ;AAAA,IACV,CAAC;AAAA,EACH,OAAO;AACL,UAAM,UACJ,OAAO,OAAO,SAAS,MAAM,WAAY,OAAO,SAAS,IAAe;AAC1E,UAAM,YAAY,mBAAmB,OAAO;AAC5C,QAAI,cAAc,MAAM;AACtB,aAAO,KAAK;AAAA,QACV,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,QACE;AAAA,MACJ,CAAC;AAAA,IACH,WAAW,cAAc,oBAAoB;AAC3C,aAAO,KAAK;AAAA,QACV,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,QACE;AAAA,MACJ,CAAC;AAAA,IACH,OAAO;AACL,aAAO,KAAK;AAAA,QACV,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,QAAQ;AAAA,MACV,CAAC;AAAA,IACH;AAAA,EACF;AAGA,QAAM,OAAO,MAAM,SAAS,IAAI;AAChC,MAAI,SAAS,MAAM;AACjB,WAAO,KAAK;AAAA,MACV,MAAM;AAAA,MACN,IAAI;AAAA,MACJ,QACE;AAAA,IACJ,CAAC;AAAA,EACH,OAAO;AACL,WAAO,KAAK;AAAA,MACV,MAAM;AAAA,MACN,IAAI;AAAA,MACJ,QAAQ,kCAAkC,KAAK,eAAe,eAAe,KAAK,WAAW;AAAA,IAC/F,CAAC;AAAA,EACH;AAGA,MAAI,MAAM,WAAW,cAAc,IAAI,CAAC,GAAG;AACzC,WAAO,KAAK;AAAA,MACV,MAAM;AAAA,MACN,IAAI;AAAA,MACJ,QAAQ,4CAA4C,mBAAmB;AAAA,IACzE,CAAC;AAAA,EACH,OAAO;AACL,WAAO,KAAK;AAAA,MACV,MAAM;AAAA,MACN,IAAI;AAAA,MACJ,QAAQ,0CAA0C,mBAAmB;AAAA,IACvE,CAAC;AAAA,EACH;AAGA,MAAI,QAAQ;AACV,UAAM,QACJ,OAAO,OAAO,KAAK,OAAO,OAAO,OAAO,MAAM,YAAY,CAAC,MAAM,QAAQ,OAAO,OAAO,CAAC,IACnF,OAAO,OAAO,IACf,CAAC;AACP,UAAM,eAAyB,CAAC;AAChC,eAAW,CAAC,UAAU,SAAS,KAAK,OAAO,QAAQ,UAAU,GAAG;AAC9D,YAAM,OAAO,MAAM,QAAQ,MAAM,QAAQ,CAAC,IAAK,MAAM,QAAQ,IAAiB,CAAC;AAC/E,iBAAW,QAAQ,WAAW;AAC5B,YAAI,CAAC,KAAK,SAAS,IAAI,EAAG,cAAa,KAAK,QAAQ;AAAA,MACtD;AAAA,IACF;AACA,QAAI,aAAa,SAAS,GAAG;AAC3B,aAAO,KAAK;AAAA,QACV,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,QAAQ,2BAA2B,CAAC,GAAG,IAAI,IAAI,YAAY,CAAC,EAAE,KAAK,IAAI,CAAC;AAAA,MAC1E,CAAC;AAAA,IACH,OAAO;AACL,aAAO,KAAK,EAAE,MAAM,SAAS,IAAI,MAAM,QAAQ,mCAAmC,CAAC;AAAA,IACrF;AAAA,EACF;AAEA,QAAM,KAAK,OAAO,MAAM,CAAC,MAAM,EAAE,EAAE;AACnC,SAAO,EAAE,IAAI,OAAO;AACtB;AAYA,eAAsB,0BACpB,MACA,UAAgC,CAAC,GACD;AAChC,QAAM,EAAE,SAAS,MAAM,IAAI;AAC3B,QAAM,UAAoB,CAAC;AAK3B,QAAM,YAAY,MAAM,SAAS,IAAI,IAAI;AACzC,QAAM,UAAU,sBAAsB;AACtC,QAAM,eAAyC,CAAC;AAChD,aAAW,OAAO,oBAAI,IAAI;AAAA,IACxB,GAAG,OAAO,KAAK,QAAQ,KAAK;AAAA,IAC5B,GAAG,OAAO,KAAK,UAAU,SAAS,CAAC,CAAC;AAAA,EACtC,CAAC,GAAG;AACF,iBAAa,GAAG,IAAI;AAAA,MAClB,GAAG,oBAAI,IAAI,CAAC,GAAI,QAAQ,MAAM,GAAG,KAAK,CAAC,GAAI,GAAI,UAAU,MAAM,GAAG,KAAK,CAAC,CAAE,CAAC;AAAA,IAC7E;AAAA,EACF;AACA,QAAM,oBAA8C,CAAC;AACrD,aAAW,OAAO,oBAAI,IAAI;AAAA,IACxB,GAAG,OAAO,KAAK,QAAQ,UAAU;AAAA,IACjC,GAAG,OAAO,KAAK,UAAU,cAAc,CAAC,CAAC;AAAA,EAC3C,CAAC,GAAG;AACF,sBAAkB,GAAG,IAAI;AAAA,MACvB,GAAG,oBAAI,IAAI,CAAC,GAAI,QAAQ,WAAW,GAAG,KAAK,CAAC,GAAI,GAAI,UAAU,WAAW,GAAG,KAAK,CAAC,CAAE,CAAC;AAAA,IACvF;AAAA,EACF;AAEA,QAAM,SAAS,MAAM,mBAAmB,IAAI;AAC5C,MAAI,WAAW,MAAM;AACnB,QAAI,gBAAgB;AAGpB,UAAM,UACJ,OAAO,OAAO,SAAS,MAAM,WAAY,OAAO,SAAS,IAAe;AAC1E,QAAI,SAAS;AACX,YAAM,YAAY,mBAAmB,OAAO;AAC5C,UAAI,cAAc,MAAM;AACtB,cAAM,UAAU,QACb,QAAQ,WAAW,EAAE,EACrB,QAAQ,WAAW,MAAM,EACzB,QAAQ;AACX,YAAI,YAAY,IAAI;AAClB,iBAAO,OAAO,SAAS;AAAA,QACzB,OAAO;AACL,iBAAO,SAAS,IAAI;AAAA,QACtB;AACA,wBAAgB;AAAA,MAClB;AAAA,IACF;AAGA,UAAM,QACJ,OAAO,OAAO,KAAK,OAAO,OAAO,OAAO,MAAM,YAAY,CAAC,MAAM,QAAQ,OAAO,OAAO,CAAC,IACnF,OAAO,OAAO,IACf;AACN,QAAI,OAAO;AACT,UAAI,eAAe;AACnB,iBAAW,CAAC,UAAU,SAAS,KAAK,OAAO,QAAQ,YAAY,GAAG;AAChE,YAAI,CAAC,MAAM,QAAQ,MAAM,QAAQ,CAAC,EAAG;AACrC,cAAM,OAAO,MAAM,QAAQ;AAC3B,cAAM,WAAW,KAAK,OAAO,CAAC,MAAM,CAAC,UAAU,SAAS,CAAC,CAAC;AAC1D,YAAI,SAAS,WAAW,KAAK,QAAQ;AACnC,cAAI,SAAS,WAAW,GAAG;AACzB,mBAAO,MAAM,QAAQ;AAAA,UACvB,OAAO;AACL,kBAAM,QAAQ,IAAI;AAAA,UACpB;AACA,yBAAe;AAAA,QACjB;AAAA,MACF;AACA,UAAI,cAAc;AAChB,YAAI,OAAO,KAAK,KAAK,EAAE,WAAW,GAAG;AACnC,iBAAO,OAAO,OAAO;AAAA,QACvB;AACA,wBAAgB;AAAA,MAClB;AAAA,IACF;AAGA,UAAM,aACJ,OAAO,YAAY,KACnB,OAAO,OAAO,YAAY,MAAM,YAChC,CAAC,MAAM,QAAQ,OAAO,YAAY,CAAC,IAC9B,OAAO,YAAY,IACpB;AACN,QAAI,YAAY;AACd,UAAI,aAAa;AACjB,iBAAW,SAAS,CAAC,SAAS,SAAS,GAAY;AACjD,cAAM,eAAe,kBAAkB,KAAK,KAAK,CAAC;AAClD,cAAM,KACJ,WAAW,KAAK,KAChB,OAAO,WAAW,KAAK,MAAM,YAC7B,CAAC,MAAM,QAAQ,WAAW,KAAK,CAAC,IAC3B,WAAW,KAAK,IACjB;AACN,YAAI,CAAC,MAAM,CAAC,MAAM,QAAQ,GAAG,UAAU,CAAC,EAAG;AAC3C,cAAM,OAAO,GAAG,UAAU;AAC1B,cAAM,WAAW,KAAK,OAAO,CAAC,MAAM,CAAC,aAAa,SAAS,CAAC,CAAC;AAC7D,YAAI,SAAS,WAAW,KAAK,QAAQ;AACnC,cAAI,SAAS,WAAW,GAAG;AACzB,mBAAO,GAAG,UAAU;AAAA,UACtB,OAAO;AACL,eAAG,UAAU,IAAI;AAAA,UACnB;AACA,uBAAa;AAAA,QACf;AACA,YAAI,OAAO,KAAK,EAAE,EAAE,WAAW,GAAG;AAChC,iBAAO,WAAW,KAAK;AAAA,QACzB;AAAA,MACF;AACA,UAAI,YAAY;AACd,YAAI,OAAO,KAAK,UAAU,EAAE,WAAW,GAAG;AACxC,iBAAO,OAAO,YAAY;AAAA,QAC5B;AACA,wBAAgB;AAAA,MAClB;AAAA,IACF;AAEA,QAAI,iBAAiB,CAAC,QAAQ;AAC5B,YAAM,aAAa,gBAAgB,MAAM;AACzC,YAAM,UAAU,WAAW,IAAI,GAAG,YAAY,MAAM;AACpD,cAAQ,KAAK,eAAe;AAAA,IAC9B,WAAW,iBAAiB,QAAQ;AAClC,cAAQ,KAAK,eAAe;AAAA,IAC9B;AAAA,EACF;AAGA,MAAI,MAAM,WAAW,cAAc,IAAI,CAAC,GAAG;AACzC,QAAI,CAAC,QAAQ;AACX,YAAM,GAAG,cAAc,IAAI,GAAG,EAAE,OAAO,KAAK,CAAC;AAAA,IAC/C;AACA,YAAQ,KAAK,mBAAmB;AAAA,EAClC;AAGA,MAAI,MAAM,WAAW,SAAS,IAAI,CAAC,GAAG;AACpC,QAAI,CAAC,QAAQ;AACX,YAAM,GAAG,SAAS,IAAI,GAAG,EAAE,OAAO,KAAK,CAAC;AAAA,IAC1C;AACA,YAAQ,KAAK,aAAa;AAAA,EAC5B;AAEA,SAAO,EAAE,QAAQ;AACnB;","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 canonical payload. Wrap it in the document's native comment (\\`<!-- ... -->\\` in Markdown) on its own line directly under the text it grounds.\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/) carries exactly one explicit scope declaration: \\`pdac-scope: cited\\` for a bound document that carries at least one citation, or \\`pdac-scope: none\\` with a non-empty human-authored reason (\\`pdac-scope-reason: <why>\\` in frontmatter, or \\`<!-- pdac-scope: none reason=\\\"<why>\\\" -->\\`) for an exempt one. Citations alone never bind: a document without a declaration is unclassified and fails verification, a bound document with zero citations fails, and an exemption without a reason or contradicted by citations fails. Binding and exemption are human declarations: never declare an exemption merely because citations are missing. Verify with \\`npx prodshape citations verify --provider openspec\\`: it enumerates the expected current documents, so zero discovered citations is a set of failures, never a pass. Archived changes are excluded by default; \\`--include-archived\\` also verifies the citations history carries, 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 a citation payload emitted by `npx prodshape cite --id <ID> --digest <digest>` and wrapped in the document native comment (`<!-- ... -->` in Markdown). 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, each with an explicit declaration: declare `pdac-scope: cited` on a line of its own and cite the canonical text the document depends on, or declare `pdac-scope: none` with a non-empty reason (`pdac-scope-reason: <why>` in frontmatter, or `<!-- pdac-scope: none reason=\"<why>\" -->`) when a human judges the document has no product-semantic dependency. Citations alone never bind, and 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, every bound document without citations and every exemption without a reason. 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 { readFileSync } from 'node:fs';\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/** The integration version, read once from this package's own manifest. */\nconst integrationVersion = (\n JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as {\n version: string;\n }\n).version;\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 version: integrationVersion,\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.4.0",
3
+ "version": "0.5.1",
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.16.0"
30
+ "@prodshape/core": "0.18.0"
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,25 +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) and is the single source of truth for product behaviour. OpenSpec artifacts are downstream consumers of it, never a second source of truth.
48
- Consumers 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.
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
- Finding which artifacts a change impacts is understanding work: 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 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 document population, so zero discovered citations is a set of failures, never a pass. The scope gate applies to current documents; citations in archived changes are still checked, but their problems are reported as warnings, because archived history cannot be edited and its drift is information for the reader.
52
- The 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.
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
+ To cite: run \`npx prodshape inspect <ID>\` to read the current digest, then \`npx prodshape cite --id <ID> --digest <digest>\` to emit the canonical payload. Wrap it in the document's native comment (\`<!-- ... -->\` in Markdown) on its own line directly under the text it grounds.
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/) carries exactly one explicit scope declaration: \`pdac-scope: cited\` for a bound document that carries at least one citation, or \`pdac-scope: none\` with a non-empty human-authored reason (\`pdac-scope-reason: <why>\` in frontmatter, or \`<!-- pdac-scope: none reason=\"<why>\" -->\`) for an exempt one. Citations alone never bind: a document without a declaration is unclassified and fails verification, a bound document with zero citations fails, and an exemption without a reason or contradicted by citations fails. Binding and exemption are human declarations: never declare an exemption merely because citations are missing. Verify with \`npx prodshape citations verify --provider openspec\`: it enumerates the expected current documents, so zero discovered citations is a set of failures, never a pass. Archived changes are excluded by default; \`--include-archived\` also verifies the citations history carries, 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.
53
53
  ${PDAC_CONTEXT_END}`;
54
54
 
55
55
  /** The PDaC citation rules injected per artifact into openspec/config.yaml `rules:`. */
56
56
  export const PDAC_RULES: Record<string, string[]> = {
57
57
  proposal: [
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. This is understanding work, not lookup: 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 with the graph: 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, so nothing is dropped silently.',
58
+ 'State which PDaC artifacts this change touches, each with a citation payload emitted by `npx prodshape cite --id <ID> --digest <digest>` and wrapped in the document native comment (`<!-- ... -->` in Markdown). 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
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.',
61
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.',
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.',
62
+ 'Every document of this change must end up bound or exempt, each with an explicit declaration: declare `pdac-scope: cited` on a line of its own and cite the canonical text the document depends on, or declare `pdac-scope: none` with a non-empty reason (`pdac-scope-reason: <why>` in frontmatter, or `<!-- pdac-scope: none reason="<why>" -->`) when a human judges the document has no product-semantic dependency. Citations alone never bind, and never declare an exemption just because citations are missing.',
63
63
  ],
64
64
  specs: [
65
- '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.',
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.',
66
66
  'Cite a specific verification scenario with an anchor when the requirement maps to one (anchor="<scenario id>").',
67
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 -->).',
68
68
  'Never invent artifact ids or digests. Read them with `npx prodshape inspect <ID>`.',
@@ -83,7 +83,7 @@ export const PDAC_OPERATIONS: {
83
83
  } = {
84
84
  apply: {
85
85
  guidance: [
86
- '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.',
86
+ 'Before finishing, run `npx prodshape citations verify --provider openspec` and fix every stale, tampered or unresolved citation, every unclassified document, every bound document without citations and every exemption without a reason. Refresh a stale digest by re-running `npx prodshape inspect <ID>`; never delete a citation or declare `pdac-scope: none` to silence a diagnostic.',
87
87
  ],
88
88
  },
89
89
  archive: {
@@ -229,23 +229,12 @@ function compareVersions(a: string, b: string): number {
229
229
  }
230
230
 
231
231
  /** Run a command at the repository root, resolving stdout+stderr or rejecting on failure. */
232
- function runAtRoot(root: string, command: string, args: string[]): Promise<string> {
233
- return new Promise((resolve, reject) => {
234
- // Commands are typically .cmd shims on Windows; `shell: true` lets execFile find them on PATH
235
- // across platforms without callers needing to know the extension.
236
- execFile(
237
- command,
238
- args,
239
- { cwd: root, shell: true, env: envWithLocalBin(root) },
240
- (error, stdout, stderr) => {
241
- if (error) {
242
- reject(new Error(`${command} ${args.join(' ')} failed: ${error.message}`));
243
- return;
244
- }
245
- resolve(`${stdout}${stderr}`);
246
- },
247
- );
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),
248
236
  });
237
+ return `${stdout}${stderr}`;
249
238
  }
250
239
 
251
240
  /**
@@ -255,31 +244,23 @@ function runAtRoot(root: string, command: string, args: string[]): Promise<strin
255
244
  export async function detectOpenSpecVersion(
256
245
  root?: string,
257
246
  ): Promise<{ version: string } | { error: string }> {
258
- return new Promise((resolve) => {
259
- // `openspec` is typically a .cmd shim on Windows; `shell: true` lets execFile find it on PATH
260
- // across platforms without callers needing to know the extension.
261
- execFile(
262
- 'openspec',
263
- ['--version'],
264
- { shell: true, ...(root ? { env: envWithLocalBin(root) } : {}) },
265
- (error, stdout, stderr) => {
266
- if (error) {
267
- resolve({
268
- error: `OpenSpec CLI not found on PATH (run: npm install -g ${OPENSPEC_NPX_SPEC}). ${error.message}`,
269
- });
270
- return;
271
- }
272
- const output = `${stdout}${stderr}`.trim();
273
- // `openspec --version` prints something like "1.3.0" or "openspec/1.3.0 ...".
274
- const match = output.match(/(\d+\.\d+\.\d+(?:-[^\s]+)?)/);
275
- if (!match?.[1]) {
276
- resolve({ error: `Could not parse OpenSpec version from output: ${output}` });
277
- return;
278
- }
279
- resolve({ version: match[1] });
280
- },
281
- );
282
- });
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
+ }
283
264
  }
284
265
 
285
266
  /**
package/src/population.ts CHANGED
@@ -8,10 +8,9 @@
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';
11
+ import { readFileSync } from 'node:fs';
12
12
  import { readdir, readFile } from 'node:fs/promises';
13
13
  import { join, relative, sep } from 'node:path';
14
- import { promisify } from 'node:util';
15
14
  import fg from 'fast-glob';
16
15
  import { codes } from '@prodshape/core';
17
16
  import type {
@@ -21,10 +20,9 @@ import type {
21
20
  EnumerateConsumerDocumentsOptions,
22
21
  SddIntegrationProvider,
23
22
  } from '@prodshape/core';
23
+ import { runCommand } from './process.js';
24
24
  import { envWithLocalBin, isOpenSpecWorkspace, pathExists } from './workspace.js';
25
25
 
26
- const execFileAsync = promisify(execFile);
27
-
28
26
  /**
29
27
  * The artifact files that make up an OpenSpec change, relative to the change directory.
30
28
  * `specs/` is a directory containing one or more `<capability>/spec.md` files.
@@ -40,12 +38,8 @@ async function runOpenSpec(
40
38
  repoRoot: string,
41
39
  args: string[],
42
40
  ): Promise<{ stdout: string; stderr: string }> {
43
- // `openspec` is typically a .cmd shim on Windows; `shell: true` lets execFile find it on PATH
44
- // across platforms. The repository's node_modules/.bin is consulted first, so a
45
- // devDependency install counts.
46
- return execFileAsync('openspec', args, {
41
+ return runCommand('openspec', args, {
47
42
  cwd: repoRoot,
48
- shell: true,
49
43
  env: envWithLocalBin(repoRoot),
50
44
  maxBuffer: 10 * 1024 * 1024,
51
45
  });
@@ -74,9 +68,8 @@ async function openspecListChanges(repoRoot: string): Promise<string[] | null> {
74
68
  /** Check whether `openspec` is runnable (PATH or node_modules/.bin) with a parseable version. */
75
69
  export async function isOpenSpecCliAvailable(repoRoot?: string): Promise<boolean> {
76
70
  try {
77
- const { stdout } = await execFileAsync('openspec', ['--version'], {
78
- shell: true,
79
- ...(repoRoot ? { env: envWithLocalBin(repoRoot) } : {}),
71
+ const { stdout } = await runCommand('openspec', ['--version'], {
72
+ ...(repoRoot ? { cwd: repoRoot, env: envWithLocalBin(repoRoot) } : {}),
80
73
  });
81
74
  return /\d+\.\d+\.\d+/.test(stdout);
82
75
  } catch {
@@ -119,7 +112,6 @@ async function discoverChangeDocuments(
119
112
  ): Promise<ConsumerDocument[]> {
120
113
  const docs: ConsumerDocument[] = [];
121
114
 
122
- // Fixed artifact files: proposal.md, design.md, tasks.md.
123
115
  for (const { file, kind } of CHANGE_ARTIFACT_FILES) {
124
116
  const absolutePath = join(changeDir, file);
125
117
  try {
@@ -132,7 +124,6 @@ async function discoverChangeDocuments(
132
124
  }
133
125
  }
134
126
 
135
- // specs/ directory: one or more <capability>/spec.md files.
136
127
  const specsDir = join(changeDir, 'specs');
137
128
  try {
138
129
  const specFiles = await discoverMarkdownFiles(specsDir);
@@ -212,10 +203,10 @@ export async function enumerateOpenSpecDocuments(
212
203
  const documents: ConsumerDocument[] = [];
213
204
  const diagnostics: Diagnostic[] = [];
214
205
 
215
- // 1. Discover current changes. A workspace with no `openspec/changes/` directory at all (specs
216
- // only) has zero changes by construction: nothing to discover, no CLI needed, and no PRODUCT069
217
- // — older OpenSpec CLIs (1.3.x) even reject `list` outright in such a workspace. Otherwise
218
- // prefer `openspec list --json` and fall back to filesystem scanning.
206
+ // A workspace with no `openspec/changes/` directory at all (specs only) has zero changes by
207
+ // construction: nothing to discover, no CLI needed, and no PRODUCT069 — older OpenSpec CLIs
208
+ // (1.3.x) even reject `list` outright in such a workspace. Otherwise prefer
209
+ // `openspec list --json` and fall back to filesystem scanning.
219
210
  let currentChangeNames: string[] | null = (await pathExists(changesDir))
220
211
  ? await openspecListChanges(repoRoot)
221
212
  : [];
@@ -227,8 +218,7 @@ export async function enumerateOpenSpecDocuments(
227
218
  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.`,
228
219
  file: 'openspec/',
229
220
  });
230
- // CLI unavailable or unparseable: fall back to filesystem scanning of openspec/changes/,
231
- // excluding archive/ (it's handled separately below).
221
+ // archive/ is excluded here: archived changes are enumerated separately below.
232
222
  try {
233
223
  const entries = await readdir(changesDir, { withFileTypes: true });
234
224
  currentChangeNames = entries
@@ -240,7 +230,6 @@ export async function enumerateOpenSpecDocuments(
240
230
  }
241
231
  }
242
232
 
243
- // 2. Filter to a single change if requested.
244
233
  if (options?.change) {
245
234
  const wanted = options.change;
246
235
  if (currentChangeNames.includes(wanted)) {
@@ -251,13 +240,11 @@ export async function enumerateOpenSpecDocuments(
251
240
  }
252
241
  }
253
242
 
254
- // 3. Discover documents for each current change.
255
243
  for (const name of currentChangeNames) {
256
244
  const changeDir = join(changesDir, name);
257
245
  documents.push(...(await discoverChangeDocuments(changeDir, repoRoot, name, false)));
258
246
  }
259
247
 
260
- // 4. Discover archived changes if requested.
261
248
  if (includeArchived) {
262
249
  const archivedNames = await discoverArchivedChanges(archiveDir);
263
250
  for (const name of archivedNames) {
@@ -268,7 +255,6 @@ export async function enumerateOpenSpecDocuments(
268
255
  }
269
256
  }
270
257
 
271
- // 5. Discover current specs (openspec/specs/).
272
258
  documents.push(...(await discoverSpecDocuments(specsDir, repoRoot)));
273
259
 
274
260
  return {
@@ -279,12 +265,20 @@ export async function enumerateOpenSpecDocuments(
279
265
  };
280
266
  }
281
267
 
268
+ /** The integration version, read once from this package's own manifest. */
269
+ const integrationVersion = (
270
+ JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as {
271
+ version: string;
272
+ }
273
+ ).version;
274
+
282
275
  /**
283
276
  * The OpenSpec implementation of the SDD integration-provider contract.
284
277
  * `prodshape citations verify --provider openspec` verifies the population this enumerates.
285
278
  */
286
279
  export const openSpecProvider: SddIntegrationProvider = {
287
280
  name: 'openspec',
281
+ version: integrationVersion,
288
282
  detectWorkspace: isOpenSpecWorkspace,
289
283
  enumerateDocuments: enumerateOpenSpecDocuments,
290
284
  };
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
+ }