@prodshape/integration-openspec 0.6.0-alpha.0 → 0.6.0-alpha.3

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.
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * The deterministic rails of the OpenSpec-hosted PDaC product workflow.
3
3
  *
4
- * An OpenSpec change created with the `product` schema hosts a PDaC Product Change at
5
- * `openspec/changes/<name>/product/` (change.md plus proposed/**, the normative delta format).
4
+ * An OpenSpec change created with the `product-change` schema hosts a PDaC Product Change at
5
+ * `openspec/changes/<name>/product-change/` (change.md plus proposed/**, the normative delta format).
6
6
  * OpenSpec owns the workflow orchestration (`/opsx:new`, `/opsx:continue`, `/opsx:apply`,
7
7
  * `/opsx:archive`); this module owns what OpenSpec cannot provide: reading the accepted model,
8
8
  * compiling the semantic graph in memory, validating the hosted delta as an overlay on the
@@ -31,18 +31,17 @@ import { join } from 'node:path';
31
31
  import { parse } from 'yaml';
32
32
  import {
33
33
  analyzeImpact,
34
- blockingDiagnostics,
35
34
  compareCodePoints,
36
- dedupeDiagnostics,
37
35
  discoverChanges,
38
36
  executeApply,
39
37
  isNotFound,
40
38
  loadChange,
41
39
  openRepository,
42
- planApply,
40
+ planHostedProductChange,
43
41
  preflightApply,
44
42
  sortDiagnostics,
45
43
  validateBaseline,
44
+ validateHostedProductChange,
46
45
  validateChange,
47
46
  } from '@prodshape/core';
48
47
  import type {
@@ -55,11 +54,11 @@ import type {
55
54
  ProductGraph,
56
55
  ProductRepository,
57
56
  } from '@prodshape/core';
58
- import { OPENSPEC_PRODUCT_SCHEMA_NAME } from './product-schema.js';
57
+ import { OPENSPEC_PRODUCT_CHANGE_SCHEMA_NAME } from './product-change-schema.js';
59
58
  import { pathExists } from './workspace.js';
60
59
 
61
60
  /** The subdirectory of an OpenSpec change that hosts the PDaC Product Change delta. */
62
- export const OPENSPEC_PRODUCT_SUBDIR = 'product';
61
+ export const OPENSPEC_PRODUCT_SUBDIR = 'product-change';
63
62
 
64
63
  /** OpenSpec 1.11's change-id grammar (`isKebabId`): one folder-safe path segment. */
65
64
  const OPENSPEC_CHANGE_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
@@ -101,7 +100,7 @@ export async function inspectProductModel(root: string): Promise<ProductModelIns
101
100
  export interface OpenSpecProductChangeRef {
102
101
  /** The OpenSpec change name (the directory under openspec/changes/). */
103
102
  name: string;
104
- /** Absolute path of the hosted PDaC change directory (…/<name>/product). */
103
+ /** Absolute path of the hosted PDaC change directory (…/<name>/product-change). */
105
104
  dir: string;
106
105
  /** Repository-relative POSIX path of the hosted change.md. */
107
106
  file: string;
@@ -171,7 +170,7 @@ function openSpecChangeMetadataProblem(parsed: unknown): string | undefined {
171
170
 
172
171
  /**
173
172
  * Read the schema a change is pinned to. OpenSpec records it in the change's `.openspec.yaml`
174
- * (`openspec new change <name> --schema product` writes it), and the hosted product rail treats
173
+ * (`openspec new change <name> --schema product-change` writes it), and the hosted product rail treats
175
174
  * that pin as load-bearing: OpenSpec selects the workflow, so a change is a product change
176
175
  * because its container says so, never because a `product/` directory happens to exist.
177
176
  */
@@ -198,7 +197,7 @@ function hostedSchemaPinError(
198
197
  ): Error {
199
198
  if (pin.problem === 'missing') {
200
199
  return new Error(
201
- `OpenSpec change '${changeName}' has no .openspec.yaml, so its schema pin is unknown; the hosted product rail refuses it. Create product changes with: openspec new change ${changeName} --schema ${OPENSPEC_PRODUCT_SCHEMA_NAME}.`,
200
+ `OpenSpec change '${changeName}' has no .openspec.yaml, so its schema pin is unknown; the hosted product rail refuses it. Create product changes with: openspec new change ${changeName} --schema ${OPENSPEC_PRODUCT_CHANGE_SCHEMA_NAME}.`,
202
201
  );
203
202
  }
204
203
  return new Error(
@@ -207,8 +206,8 @@ function hostedSchemaPinError(
207
206
  }
208
207
 
209
208
  /**
210
- * List the OpenSpec changes that host a PDaC Product Change: pinned to `schema: product` in
211
- * `.openspec.yaml` AND carrying `product/change.md`. The OpenSpec archive is history and is never
209
+ * List the OpenSpec changes that host a PDaC Product Change: pinned to `schema: product-change` in
210
+ * `.openspec.yaml` AND carrying `product-change/change.md`. The OpenSpec archive is history and is never
212
211
  * listed; a change without a product delta (for example one whose honest intent verdict was "no
213
212
  * product delta") is not a product change; and a change pinned to another schema is not one
214
213
  * either, whatever directories it contains, so it never enters the product rail or its
@@ -237,7 +236,7 @@ export async function listOpenSpecProductChanges(
237
236
  assertOpenSpecChangeName(name);
238
237
  const pin = await readHostedSchemaPin(changeDir);
239
238
  if ('problem' in pin) throw hostedSchemaPinError(name, pin);
240
- if (pin.schema !== OPENSPEC_PRODUCT_SCHEMA_NAME) continue;
239
+ if (pin.schema !== OPENSPEC_PRODUCT_CHANGE_SCHEMA_NAME) continue;
241
240
  refs.push({
242
241
  name,
243
242
  dir,
@@ -273,7 +272,7 @@ async function loadHostedChange(
273
272
  const refs = await listOpenSpecProductChanges(root);
274
273
  const known = refs.map((candidate) => candidate.name).join(', ') || 'none';
275
274
  throw new Error(
276
- `No OpenSpec product change named '${changeName}' under openspec/changes/ (a product change hosts product/change.md; found: ${known}).`,
275
+ `No OpenSpec product change named '${changeName}' under openspec/changes/ (a product change hosts product-change/change.md; found: ${known}).`,
277
276
  );
278
277
  }
279
278
  // The schema pin is load-bearing and fails closed: without a readable pin naming the product
@@ -282,9 +281,9 @@ async function loadHostedChange(
282
281
  if ('problem' in pin) {
283
282
  throw hostedSchemaPinError(changeName, pin);
284
283
  }
285
- if (pin.schema !== OPENSPEC_PRODUCT_SCHEMA_NAME) {
284
+ if (pin.schema !== OPENSPEC_PRODUCT_CHANGE_SCHEMA_NAME) {
286
285
  throw new Error(
287
- `OpenSpec change '${changeName}' is pinned to schema '${pin.schema}', not '${OPENSPEC_PRODUCT_SCHEMA_NAME}'; the hosted product rail refuses it. Product intent travels through a change created with: openspec new change <name> --schema ${OPENSPEC_PRODUCT_SCHEMA_NAME}.`,
286
+ `OpenSpec change '${changeName}' is pinned to schema '${pin.schema}', not '${OPENSPEC_PRODUCT_CHANGE_SCHEMA_NAME}'; the hosted product rail refuses it. Product intent travels through a change created with: openspec new change <name> --schema ${OPENSPEC_PRODUCT_CHANGE_SCHEMA_NAME}.`,
288
287
  );
289
288
  }
290
289
  return loadChange(deltaDir, repo.root, repo.registry);
@@ -349,20 +348,21 @@ export async function validateOpenSpecProductChange(
349
348
  all.find((candidate) => hostedName(root, candidate) === changeName) ??
350
349
  (await loadHostedChange(root, repo, changeName));
351
350
  const validation = validateChange(change, baseline.artifacts, all);
352
- const diagnostics = sortDiagnostics(
353
- dedupeDiagnostics([
354
- ...repo.configDiagnostics,
355
- ...baseline.diagnostics,
356
- ...validation.diagnostics,
357
- ]),
351
+ const hosted = validateHostedProductChange(
352
+ repo,
353
+ baseline.artifacts,
354
+ change,
355
+ all,
356
+ baseline.diagnostics,
358
357
  );
358
+ const diagnostics = hosted.diagnostics;
359
359
  return {
360
360
  change,
361
361
  baseline,
362
362
  overlayArtifacts: validation.overlayArtifacts,
363
363
  overlayGraph: validation.overlayGraph,
364
364
  diagnostics,
365
- blocking: blockingDiagnostics(diagnostics, repo.config.validation['warnings-as-errors']),
365
+ blocking: hosted.blocking,
366
366
  };
367
367
  }
368
368
 
@@ -373,7 +373,7 @@ function hostedName(root: string, change: LoadedChange): string | undefined {
373
373
  const relative = normalizedDir.startsWith(normalizedRoot)
374
374
  ? normalizedDir.slice(normalizedRoot.length).replace(/^\//, '')
375
375
  : normalizedDir;
376
- const match = relative.match(/^openspec\/changes\/([^/]+)\/product$/);
376
+ const match = relative.match(/^openspec\/changes\/([^/]+)\/product-change$/);
377
377
  return match?.[1];
378
378
  }
379
379
 
@@ -419,7 +419,13 @@ export async function applyOpenSpecProductChange(
419
419
  all.find((candidate) => hostedName(root, candidate) === changeName) ??
420
420
  (await loadHostedChange(root, repo, changeName));
421
421
 
422
- const validation = validateChange(change, baseline.artifacts, all);
422
+ const hosted = validateHostedProductChange(
423
+ repo,
424
+ baseline.artifacts,
425
+ change,
426
+ all,
427
+ baseline.diagnostics,
428
+ );
423
429
  // Everything capable of making the resulting model invalid blocks BEFORE any write:
424
430
  // configuration diagnostics, the baseline's own load and per-document diagnostics, the change's
425
431
  // diagnostics and the full overlay revalidation. The overlay pass alone is not enough, because
@@ -427,24 +433,13 @@ export async function applyOpenSpecProductChange(
427
433
  // body-section defects) are load-time diagnostics that graph-level revalidation never re-emits;
428
434
  // omitting them let apply write into a model whose fresh validation then failed, violating the
429
435
  // resulting-model obligation. Refusal happens with the working tree byte-identical.
430
- const overlayErrors = blockingDiagnostics(
431
- sortDiagnostics(
432
- dedupeDiagnostics([
433
- ...repo.configDiagnostics,
434
- ...baseline.diagnostics,
435
- ...validation.diagnostics,
436
- ]),
437
- ),
438
- repo.config.validation['warnings-as-errors'],
439
- );
440
-
441
- const rawPlan = await planApply({
436
+ const rawPlan = await planHostedProductChange({
442
437
  repoRoot: repo.root,
443
438
  modelRelative: repo.config.product.model,
444
439
  changesRelative: repo.config.product.changes,
445
440
  change,
446
441
  baseline: baseline.artifacts,
447
- overlayErrors,
442
+ overlayErrors: hosted.blocking,
448
443
  });
449
444
  const plan: ApplyPlan = {
450
445
  ...rawPlan,
@@ -1,49 +0,0 @@
1
- ---
2
- id: CHG-EXAMPLE-001
3
- type: product-change
4
- title: Example product change
5
- status: draft
6
- base-revision: '3f2a91c'
7
- operations:
8
- add:
9
- - FR-EXAMPLE-001
10
- modify:
11
- - UC-EXAMPLE-001
12
- remove: []
13
- ---
14
-
15
- This file lives at `product/change.md` inside the OpenSpec change directory, and the proposed future state lives beside it under `product/proposed/**`, laid out exactly as it will live in `docs/product/model` with files named by lowercase id.
16
-
17
- This manifest and every artifact under `product/proposed/**` are Product Definition content, not consumer documents. Do not add `pdac-scope` declarations or PDaC citations here. Proposed artifacts express their internal connections through typed PDaC relationships; citations remain in `proposal.md` and other documents that consume the accepted model.
18
-
19
- Every ID under `operations.add` and `operations.modify` needs a complete proposed artifact under `product/proposed/`. Author each artifact from its current PDaC template (`prodshape template <kind>`). Set `base-revision` to the baseline commit this change was authored against, quoted so YAML reads an all-digit revision as a string.
20
-
21
- Leave `status: draft` while authoring; `status: approved` is the apply-authorised protocol state and setting it is the caller's authorisation act, outside this workflow. Apply materializes the proposal into the accepted model on a working branch; it never archives this change, and a pull-request merge accepts the resulting baseline. Product Change status never records implementation, verification, release or deployment.
22
-
23
- ## Problem
24
-
25
- What is wrong or missing in the current Product Definition? State the problem, not the solution.
26
-
27
- ## Intended Product Outcome
28
-
29
- What the Product Definition says once this change is accepted. Describe the destination, not the steps.
30
-
31
- ## Rationale
32
-
33
- Why this outcome, and why now. Record the reasoning a future reader would otherwise have to reconstruct.
34
-
35
- ## Affected Product Areas
36
-
37
- Which parts of the product this change touches, in product language rather than file paths.
38
-
39
- ## Open Questions
40
-
41
- Unresolved questions that need a decision before the change can be authorised. If answering a question could produce materially different product behaviour, scope, policy or acceptance, ask the product owner and stop: do not complete this delta or select an answer on the user's behalf. Write `None.` only after every product-semantic question in both the intent and this manifest has been resolved.
42
-
43
- ## Product Acceptance
44
-
45
- How a reviewer recognises that the accepted definition expresses the intended outcome.
46
-
47
- ## Out of Scope
48
-
49
- What this change explicitly does not touch, including delivery, technical design and implementation.