@prodshape/integration-openspec 0.6.0-alpha.0 → 0.6.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -8
- package/assets/{openspec-product-schema → openspec-product-change-schema}/schema.yaml +41 -10
- package/assets/{openspec-product-schema → openspec-product-change-schema}/scripts/product-apply.mjs +2 -2
- package/assets/{openspec-product-schema → openspec-product-change-schema}/scripts/product-validate.mjs +2 -2
- package/assets/openspec-product-change-schema/templates/change.md +51 -0
- package/assets/{openspec-product-schema → openspec-product-change-schema}/templates/proposal.md +12 -0
- package/assets/openspec-product-recovery-schema/schema.yaml +40 -0
- package/assets/openspec-product-recovery-schema/templates/brief.md +22 -0
- package/assets/openspec-product-recovery-schema/templates/report.md +15 -0
- package/dist/index.d.ts +21 -11
- package/dist/index.js +210 -64
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +207 -40
- package/src/{product-schema.ts → product-change-schema.ts} +29 -9
- package/src/product-workflow.ts +33 -38
- package/assets/openspec-product-schema/templates/change.md +0 -49
package/src/product-workflow.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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 ${
|
|
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 !==
|
|
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 !==
|
|
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 '${
|
|
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
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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:
|
|
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
|
|
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
|
|
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.
|