@fission-ai/openspec 1.14.0 → 1.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -31
- package/dist/cli/commands/config.d.ts +8 -0
- package/dist/cli/commands/config.js +88 -0
- package/dist/cli/commands/context.d.ts +3 -0
- package/dist/cli/commands/context.js +20 -0
- package/dist/cli/commands/doctor.d.ts +3 -0
- package/dist/cli/commands/doctor.js +18 -0
- package/dist/cli/commands/schema.d.ts +6 -0
- package/dist/cli/commands/schema.js +57 -0
- package/dist/cli/commands/spec.d.ts +3 -0
- package/dist/cli/commands/spec.js +42 -0
- package/dist/cli/commands/store.d.ts +3 -0
- package/dist/cli/commands/store.js +130 -0
- package/dist/cli/commands/workset.d.ts +3 -0
- package/dist/cli/commands/workset.js +96 -0
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +86 -43
- package/dist/commands/completion.js +4 -1
- package/dist/commands/config.d.ts +16 -7
- package/dist/commands/config.js +342 -395
- package/dist/commands/context.d.ts +8 -2
- package/dist/commands/context.js +22 -37
- package/dist/commands/doctor.d.ts +6 -7
- package/dist/commands/doctor.js +14 -27
- package/dist/commands/schema.d.ts +19 -5
- package/dist/commands/schema.js +662 -702
- package/dist/commands/spec.d.ts +13 -4
- package/dist/commands/spec.js +91 -120
- package/dist/commands/store.d.ts +27 -2
- package/dist/commands/store.js +1 -120
- package/dist/commands/workflow/default-schema.d.ts +2 -0
- package/dist/commands/workflow/default-schema.js +4 -0
- package/dist/commands/workflow/instructions.js +1 -1
- package/dist/commands/workflow/shared.d.ts +1 -1
- package/dist/commands/workflow/shared.js +1 -1
- package/dist/commands/workset.d.ts +22 -2
- package/dist/commands/workset.js +1 -88
- package/dist/core/archive.js +32 -12
- package/dist/core/artifact-graph/outputs.js +39 -1
- package/dist/core/completion-tip.js +3 -1
- package/dist/core/completions/completion-provider.js +4 -2
- package/dist/core/config-schema.d.ts +1 -1
- package/dist/core/parsers/requirement-blocks.d.ts +11 -0
- package/dist/core/parsers/requirement-blocks.js +57 -0
- package/dist/core/specs-apply.js +5 -1
- package/dist/core/templates/workflows/apply-change.js +4 -3
- package/dist/core/templates/workflows/continue-change.js +17 -0
- package/dist/core/templates/workflows/explore.js +8 -4
- package/dist/core/templates/workflows/update-change.js +4 -4
- package/dist/core/templates/workflows/verify-change.js +26 -20
- package/dist/core/validation/validator.js +23 -2
- package/dist/core/view.js +2 -26
- package/dist/utils/change-metadata.d.ts +17 -0
- package/dist/utils/change-metadata.js +27 -2
- package/dist/utils/file-system.d.ts +7 -0
- package/dist/utils/file-system.js +25 -0
- package/package.json +1 -1
- package/schemas/spec-driven/schema.yaml +28 -4
|
@@ -76,9 +76,9 @@ ${PROJECT_ROOT_GUARD}
|
|
|
76
76
|
- Recommendation: "Complete task: <description>" or "Mark as done if already implemented"
|
|
77
77
|
|
|
78
78
|
**Spec Coverage**:
|
|
79
|
-
- If status marks the spec artifact skipped by \`skip_specs: true\`, or the schema defines no spec artifact, report the spec-dependent checks as not applicable.
|
|
80
|
-
- Otherwise, \`contextFiles\` is keyed by artifact id, and artifact ids come from the active schema
|
|
81
|
-
- If delta specs exist in
|
|
79
|
+
- If status marks the spec artifact skipped by \`skip_specs: true\`, or the schema defines no spec artifact (no artifact whose \`artifactPaths.<id>.outputPath\` is under \`specs/\`), report the spec-dependent checks as not applicable.
|
|
80
|
+
- Otherwise, \`contextFiles\` is keyed by artifact id, and artifact ids come from the active schema, so do not assume an id such as \`specs\`. The spec artifacts are those whose \`artifactPaths.<id>.outputPath\` is under \`specs/\`; read their files from \`contextFiles.<id>\`. If those spec files are absent or empty, mark **Spec Coverage**, **Requirement Implementation Mapping**, and **Scenario Coverage** as not verified; do not treat any of them as clean.
|
|
81
|
+
- If delta specs exist in those spec files:
|
|
82
82
|
- Extract all requirements (marked with "### Requirement:", or listed as \`FROM:\`/\`TO:\` pairs under \`## RENAMED Requirements\`) and note the delta section each one sits under: \`## ADDED\`, \`## MODIFIED\`, \`## REMOVED\`, or \`## RENAMED Requirements\`. The section decides what the check looks for.
|
|
83
83
|
- For each ADDED or MODIFIED requirement (for MODIFIED, check the text in the delta, not the old wording):
|
|
84
84
|
- Search codebase for keywords related to the requirement
|
|
@@ -87,8 +87,11 @@ ${PROJECT_ROOT_GUARD}
|
|
|
87
87
|
- Add CRITICAL issue: "Requirement not found: <requirement name>"
|
|
88
88
|
- Recommendation: "Implement requirement X: <description>"
|
|
89
89
|
- For each REMOVED requirement, the change asks for the behavior to be gone, so invert the check:
|
|
90
|
-
-
|
|
91
|
-
-
|
|
90
|
+
- A REMOVED block carries only the requirement name and any \`**Reason**\`/\`**Migration**\` notes, so the delta does not describe the behavior itself. Before searching, read the baseline requirement in the main spec at \`<planningHome.root>/openspec/specs/<capability-path>/spec.md\`, using the same capability path as the delta spec. Match it by normalized name, scanning that file's requirement headings rather than string-matching the delta's exact title. Its body and scenarios are the evidence for the behavior that must be gone; never search from the requirement name alone.
|
|
91
|
+
- If that file holds no such requirement under this or a near-equivalent name - for example because an earlier sync already applied the removal - recover the baseline from the same file's history, in the planning repository rather than the current directory: \`git -C "<planningHome.root>" log -p -- openspec/specs/<capability-path>/spec.md\`, or \`git -C "<planningHome.root>" show <commit>^:./openspec/specs/<capability-path>/spec.md\` once you have identified the commit that applied the removal. Empty output means the pathspec matched nothing, not that there is no baseline: \`git log\` reports an unmatched path as success, so re-check the path before drawing any conclusion from silence.
|
|
92
|
+
- A baseline that adds nothing to its own title - no body beyond a restatement of the name, no scenarios - is unusable evidence, the same as a missing one. If no usable baseline is available in either place, mark **Spec Coverage** as not verified for that entry with the reason and stop there: do not search, and never count an unsearched removal as passing.
|
|
93
|
+
- Search codebase for that behavior. Matches in \`openspec/\` artifacts or docs, or in code that serves only the Migration note or an ADDED requirement, are not evidence by themselves. Report any code path that still delivers the removed behavior, including one shared with an ADDED requirement.
|
|
94
|
+
- Finding no implementation is the expected result, but only once the baseline behavior was the thing you searched for. Never report a REMOVED requirement as "Requirement not found" or recommend implementing it.
|
|
92
95
|
- If the behavior is still present:
|
|
93
96
|
- Add CRITICAL issue: "Removed requirement still implemented: <requirement name>"
|
|
94
97
|
- Recommendation: "Remove the remaining implementation at <file>:<lines>, following the requirement's Migration note if it has one"
|
|
@@ -104,7 +107,7 @@ ${PROJECT_ROOT_GUARD}
|
|
|
104
107
|
|
|
105
108
|
6. **Verify Correctness**
|
|
106
109
|
|
|
107
|
-
If the delta specs are readable and contain at least one REMOVED or RENAMED requirement but no ADDED or MODIFIED requirements (the change only removes or renames requirements), report **Requirement Implementation Mapping** and **Scenario Coverage** as **Not applicable**. The REMOVED and RENAMED checks under Spec Coverage are the evidence for such a change (each RENAMED entry is checked there against its baseline behavior), so do not mark these two checks as not verified. A delta spec with no parseable requirements at all is unusable evidence, not a removal-only change: mark these checks as not verified.
|
|
110
|
+
If the delta specs are readable and contain at least one REMOVED or RENAMED requirement but no ADDED or MODIFIED requirements (the change only removes or renames requirements), report **Requirement Implementation Mapping** and **Scenario Coverage** as **Not applicable**. The REMOVED and RENAMED checks under Spec Coverage are the evidence for such a change (each REMOVED and RENAMED entry with a usable baseline is checked there against its baseline behavior, and an entry already marked not verified there stays not verified), so do not mark these two checks as not verified. A delta spec with no parseable requirements at all is unusable evidence, not a removal-only change: mark these checks as not verified.
|
|
108
111
|
|
|
109
112
|
**Requirement Implementation Mapping**:
|
|
110
113
|
- For each ADDED or MODIFIED requirement from delta specs (REMOVED entries, and RENAMED entries without a MODIFIED block, were settled under Spec Coverage):
|
|
@@ -127,14 +130,14 @@ ${PROJECT_ROOT_GUARD}
|
|
|
127
130
|
7. **Verify Coherence**
|
|
128
131
|
|
|
129
132
|
**Design Adherence**:
|
|
130
|
-
- If the schema defines no design artifact, report **Design Adherence** as not applicable.
|
|
131
|
-
- If \`contextFiles
|
|
133
|
+
- If the schema defines no design artifact (no artifact with id \`design\`, and none whose \`artifactPaths.<id>.outputPath\` is or ends in \`design.md\`), report **Design Adherence** as not applicable.
|
|
134
|
+
- If the design artifact's \`contextFiles.<id>\` file exists:
|
|
132
135
|
- Extract key decisions (look for sections like "Decision:", "Approach:", "Architecture:")
|
|
133
136
|
- Verify implementation follows those decisions
|
|
134
137
|
- If contradiction detected:
|
|
135
138
|
- Add WARNING: "Design decision not followed: <decision>"
|
|
136
139
|
- Recommendation: "Update implementation or revise design.md to match reality"
|
|
137
|
-
- Otherwise, if \`contextFiles
|
|
140
|
+
- Otherwise, if the design artifact's \`contextFiles.<id>\` file is absent or empty: mark **Design Adherence** as not verified. With other supporting artifacts, **Code Pattern Consistency** still runs; the task-only case remains limited to task completion.
|
|
138
141
|
|
|
139
142
|
**Code Pattern Consistency**:
|
|
140
143
|
- If implementation changes cannot be identified, mark **Code Pattern Consistency** as not verified and explain the missing evidence.
|
|
@@ -158,7 +161,7 @@ ${PROJECT_ROOT_GUARD}
|
|
|
158
161
|
| Coherence | Followed/Issues |
|
|
159
162
|
\`\`\`
|
|
160
163
|
|
|
161
|
-
In each Status cell, report the results of checks that ran and \`Not verified (<reason>)\` for every skipped check. If all checks in a dimension were skipped, start the cell with \`Not verified\`. Never score a skipped check as passing. Treat every not verified or partially verified check as skipped in the final assessment. Count only ADDED and MODIFIED requirements in N, and report REMOVED and RENAMED requirements separately (for example, "1 removal confirmed, 1 rename verified"). For a change that only removes or renames requirements, the Correctness cell reads \`Not applicable (no ADDED or MODIFIED requirements)\`.
|
|
164
|
+
In each Status cell, report the results of checks that ran and \`Not verified (<reason>)\` for every skipped check. If all checks in a dimension were skipped, start the cell with \`Not verified\`. Never score a skipped check as passing. Treat every not verified or partially verified check as skipped in the final assessment. Count only ADDED and MODIFIED requirements in N, and report REMOVED and RENAMED requirements separately (for example, "1 removal confirmed, 1 rename verified", or "1 removal not verified (baseline unavailable)"). For a change that only removes or renames requirements, the Correctness cell reads \`Not applicable (no ADDED or MODIFIED requirements)\`.
|
|
162
165
|
|
|
163
166
|
**Issues by Priority**:
|
|
164
167
|
|
|
@@ -285,9 +288,9 @@ ${PROJECT_ROOT_GUARD}
|
|
|
285
288
|
- Recommendation: "Complete task: <description>" or "Mark as done if already implemented"
|
|
286
289
|
|
|
287
290
|
**Spec Coverage**:
|
|
288
|
-
- If status marks the spec artifact skipped by \`skip_specs: true\`, or the schema defines no spec artifact, report the spec-dependent checks as not applicable.
|
|
289
|
-
- Otherwise, \`contextFiles\` is keyed by artifact id, and artifact ids come from the active schema
|
|
290
|
-
- If delta specs exist in
|
|
291
|
+
- If status marks the spec artifact skipped by \`skip_specs: true\`, or the schema defines no spec artifact (no artifact whose \`artifactPaths.<id>.outputPath\` is under \`specs/\`), report the spec-dependent checks as not applicable.
|
|
292
|
+
- Otherwise, \`contextFiles\` is keyed by artifact id, and artifact ids come from the active schema, so do not assume an id such as \`specs\`. The spec artifacts are those whose \`artifactPaths.<id>.outputPath\` is under \`specs/\`; read their files from \`contextFiles.<id>\`. If those spec files are absent or empty, mark **Spec Coverage**, **Requirement Implementation Mapping**, and **Scenario Coverage** as not verified; do not treat any of them as clean.
|
|
293
|
+
- If delta specs exist in those spec files:
|
|
291
294
|
- Extract all requirements (marked with "### Requirement:", or listed as \`FROM:\`/\`TO:\` pairs under \`## RENAMED Requirements\`) and note the delta section each one sits under: \`## ADDED\`, \`## MODIFIED\`, \`## REMOVED\`, or \`## RENAMED Requirements\`. The section decides what the check looks for.
|
|
292
295
|
- For each ADDED or MODIFIED requirement (for MODIFIED, check the text in the delta, not the old wording):
|
|
293
296
|
- Search codebase for keywords related to the requirement
|
|
@@ -296,8 +299,11 @@ ${PROJECT_ROOT_GUARD}
|
|
|
296
299
|
- Add CRITICAL issue: "Requirement not found: <requirement name>"
|
|
297
300
|
- Recommendation: "Implement requirement X: <description>"
|
|
298
301
|
- For each REMOVED requirement, the change asks for the behavior to be gone, so invert the check:
|
|
299
|
-
-
|
|
300
|
-
-
|
|
302
|
+
- A REMOVED block carries only the requirement name and any \`**Reason**\`/\`**Migration**\` notes, so the delta does not describe the behavior itself. Before searching, read the baseline requirement in the main spec at \`<planningHome.root>/openspec/specs/<capability-path>/spec.md\`, using the same capability path as the delta spec. Match it by normalized name, scanning that file's requirement headings rather than string-matching the delta's exact title. Its body and scenarios are the evidence for the behavior that must be gone; never search from the requirement name alone.
|
|
303
|
+
- If that file holds no such requirement under this or a near-equivalent name - for example because an earlier sync already applied the removal - recover the baseline from the same file's history, in the planning repository rather than the current directory: \`git -C "<planningHome.root>" log -p -- openspec/specs/<capability-path>/spec.md\`, or \`git -C "<planningHome.root>" show <commit>^:./openspec/specs/<capability-path>/spec.md\` once you have identified the commit that applied the removal. Empty output means the pathspec matched nothing, not that there is no baseline: \`git log\` reports an unmatched path as success, so re-check the path before drawing any conclusion from silence.
|
|
304
|
+
- A baseline that adds nothing to its own title - no body beyond a restatement of the name, no scenarios - is unusable evidence, the same as a missing one. If no usable baseline is available in either place, mark **Spec Coverage** as not verified for that entry with the reason and stop there: do not search, and never count an unsearched removal as passing.
|
|
305
|
+
- Search codebase for that behavior. Matches in \`openspec/\` artifacts or docs, or in code that serves only the Migration note or an ADDED requirement, are not evidence by themselves. Report any code path that still delivers the removed behavior, including one shared with an ADDED requirement.
|
|
306
|
+
- Finding no implementation is the expected result, but only once the baseline behavior was the thing you searched for. Never report a REMOVED requirement as "Requirement not found" or recommend implementing it.
|
|
301
307
|
- If the behavior is still present:
|
|
302
308
|
- Add CRITICAL issue: "Removed requirement still implemented: <requirement name>"
|
|
303
309
|
- Recommendation: "Remove the remaining implementation at <file>:<lines>, following the requirement's Migration note if it has one"
|
|
@@ -313,7 +319,7 @@ ${PROJECT_ROOT_GUARD}
|
|
|
313
319
|
|
|
314
320
|
6. **Verify Correctness**
|
|
315
321
|
|
|
316
|
-
If the delta specs are readable and contain at least one REMOVED or RENAMED requirement but no ADDED or MODIFIED requirements (the change only removes or renames requirements), report **Requirement Implementation Mapping** and **Scenario Coverage** as **Not applicable**. The REMOVED and RENAMED checks under Spec Coverage are the evidence for such a change (each RENAMED entry is checked there against its baseline behavior), so do not mark these two checks as not verified. A delta spec with no parseable requirements at all is unusable evidence, not a removal-only change: mark these checks as not verified.
|
|
322
|
+
If the delta specs are readable and contain at least one REMOVED or RENAMED requirement but no ADDED or MODIFIED requirements (the change only removes or renames requirements), report **Requirement Implementation Mapping** and **Scenario Coverage** as **Not applicable**. The REMOVED and RENAMED checks under Spec Coverage are the evidence for such a change (each REMOVED and RENAMED entry with a usable baseline is checked there against its baseline behavior, and an entry already marked not verified there stays not verified), so do not mark these two checks as not verified. A delta spec with no parseable requirements at all is unusable evidence, not a removal-only change: mark these checks as not verified.
|
|
317
323
|
|
|
318
324
|
**Requirement Implementation Mapping**:
|
|
319
325
|
- For each ADDED or MODIFIED requirement from delta specs (REMOVED entries, and RENAMED entries without a MODIFIED block, were settled under Spec Coverage):
|
|
@@ -336,14 +342,14 @@ ${PROJECT_ROOT_GUARD}
|
|
|
336
342
|
7. **Verify Coherence**
|
|
337
343
|
|
|
338
344
|
**Design Adherence**:
|
|
339
|
-
- If the schema defines no design artifact, report **Design Adherence** as not applicable.
|
|
340
|
-
- If \`contextFiles
|
|
345
|
+
- If the schema defines no design artifact (no artifact with id \`design\`, and none whose \`artifactPaths.<id>.outputPath\` is or ends in \`design.md\`), report **Design Adherence** as not applicable.
|
|
346
|
+
- If the design artifact's \`contextFiles.<id>\` file exists:
|
|
341
347
|
- Extract key decisions (look for sections like "Decision:", "Approach:", "Architecture:")
|
|
342
348
|
- Verify implementation follows those decisions
|
|
343
349
|
- If contradiction detected:
|
|
344
350
|
- Add WARNING: "Design decision not followed: <decision>"
|
|
345
351
|
- Recommendation: "Update implementation or revise design.md to match reality"
|
|
346
|
-
- Otherwise, if \`contextFiles
|
|
352
|
+
- Otherwise, if the design artifact's \`contextFiles.<id>\` file is absent or empty: mark **Design Adherence** as not verified. With other supporting artifacts, **Code Pattern Consistency** still runs; the task-only case remains limited to task completion.
|
|
347
353
|
|
|
348
354
|
**Code Pattern Consistency**:
|
|
349
355
|
- If implementation changes cannot be identified, mark **Code Pattern Consistency** as not verified and explain the missing evidence.
|
|
@@ -367,7 +373,7 @@ ${PROJECT_ROOT_GUARD}
|
|
|
367
373
|
| Coherence | Followed/Issues |
|
|
368
374
|
\`\`\`
|
|
369
375
|
|
|
370
|
-
In each Status cell, report the results of checks that ran and \`Not verified (<reason>)\` for every skipped check. If all checks in a dimension were skipped, start the cell with \`Not verified\`. Never score a skipped check as passing. Treat every not verified or partially verified check as skipped in the final assessment. Count only ADDED and MODIFIED requirements in N, and report REMOVED and RENAMED requirements separately (for example, "1 removal confirmed, 1 rename verified"). For a change that only removes or renames requirements, the Correctness cell reads \`Not applicable (no ADDED or MODIFIED requirements)\`.
|
|
376
|
+
In each Status cell, report the results of checks that ran and \`Not verified (<reason>)\` for every skipped check. If all checks in a dimension were skipped, start the cell with \`Not verified\`. Never score a skipped check as passing. Treat every not verified or partially verified check as skipped in the final assessment. Count only ADDED and MODIFIED requirements in N, and report REMOVED and RENAMED requirements separately (for example, "1 removal confirmed, 1 rename verified", or "1 removal not verified (baseline unavailable)"). For a change that only removes or renames requirements, the Correctness cell reads \`Not applicable (no ADDED or MODIFIED requirements)\`.
|
|
371
377
|
|
|
372
378
|
**Issues by Priority**:
|
|
373
379
|
|
|
@@ -5,7 +5,7 @@ import { MarkdownParser } from '../parsers/markdown-parser.js';
|
|
|
5
5
|
import { ChangeParser } from '../parsers/change-parser.js';
|
|
6
6
|
import { findSpecUpdates, buildUpdatedSpec } from '../specs-apply.js';
|
|
7
7
|
import { MIN_PURPOSE_LENGTH, MAX_REQUIREMENT_TEXT_LENGTH, VALIDATION_MESSAGES } from './constants.js';
|
|
8
|
-
import { parseDeltaSpec, foldRequirementName, normalizeRequirementName, extractRequirementsSection, diffScenarioNames, describeScenarioBalance, } from '../parsers/requirement-blocks.js';
|
|
8
|
+
import { parseDeltaSpec, foldRequirementName, normalizeRequirementName, extractRequirementsSection, diffScenarioNames, describeScenarioBalance, describeUnappliedContent, } from '../parsers/requirement-blocks.js';
|
|
9
9
|
import { extractRequirementBody as extractRequirementBodyShared, containsShallOrMust as containsShallOrMustShared, countScenarios as countScenariosShared, countEmptyScenarios, } from '../parsers/requirement-text.js';
|
|
10
10
|
import { findMainSpecStructureIssues } from '../parsers/spec-structure.js';
|
|
11
11
|
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
@@ -203,6 +203,16 @@ export class Validator {
|
|
|
203
203
|
message: `Requirement "${orphan.name}" is ${where}, which is not a delta section, so it is ignored. Move it under "## ADDED Requirements", "## MODIFIED Requirements", "## REMOVED Requirements", or "## RENAMED Requirements".`,
|
|
204
204
|
});
|
|
205
205
|
}
|
|
206
|
+
// Archive carries requirement blocks only, by design. Warn so text
|
|
207
|
+
// written elsewhere is moved rather than lost without notice.
|
|
208
|
+
for (const lost of plan.unappliedContent) {
|
|
209
|
+
issues.push({
|
|
210
|
+
level: 'WARNING',
|
|
211
|
+
path: entryPath,
|
|
212
|
+
line: lost.line,
|
|
213
|
+
message: `${describeUnappliedContent(lost, true)} is not carried into the main spec by archive. Move it into a requirement, or into proposal.md or design.md.`,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
206
216
|
const sectionNames = [];
|
|
207
217
|
if (plan.sectionPresence.added)
|
|
208
218
|
sectionNames.push('## ADDED Requirements');
|
|
@@ -252,6 +262,17 @@ export class Validator {
|
|
|
252
262
|
message: this.buildMissingShallOrMustMessage(`ADDED "${block.name}"`, block.name, true),
|
|
253
263
|
});
|
|
254
264
|
}
|
|
265
|
+
// Same limit the main spec enforces after archive (#1976), so
|
|
266
|
+
// `validate <change> --strict` catches a new overlong requirement
|
|
267
|
+
// before it lands. MODIFIED is left alone: its text is the existing
|
|
268
|
+
// requirement, which the specs instruction says to keep whole.
|
|
269
|
+
if (requirementText && requirementText.length > MAX_REQUIREMENT_TEXT_LENGTH) {
|
|
270
|
+
issues.push({
|
|
271
|
+
level: 'WARNING',
|
|
272
|
+
path: entryPath,
|
|
273
|
+
message: `ADDED "${block.name}": ${VALIDATION_MESSAGES.REQUIREMENT_TOO_LONG}`,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
255
276
|
const scenarioCount = this.countScenarios(block.raw);
|
|
256
277
|
if (scenarioCount < 1) {
|
|
257
278
|
issues.push({ level: 'ERROR', path: entryPath, message: `ADDED "${block.name}" must include at least one scenario${this.emptyScenarioHint(block.raw)}` });
|
|
@@ -711,7 +732,7 @@ export class Validator {
|
|
|
711
732
|
spec.requirements.forEach((req, index) => {
|
|
712
733
|
if (req.text.length > MAX_REQUIREMENT_TEXT_LENGTH) {
|
|
713
734
|
issues.push({
|
|
714
|
-
level: '
|
|
735
|
+
level: 'WARNING',
|
|
715
736
|
path: `requirements[${index}]`,
|
|
716
737
|
message: VALIDATION_MESSAGES.REQUIREMENT_TOO_LONG,
|
|
717
738
|
});
|
package/dist/core/view.js
CHANGED
|
@@ -52,14 +52,6 @@ export class ViewCommand {
|
|
|
52
52
|
console.log(` ${chalk.green('✓')} ${change.name}`);
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
|
-
// Display archived changes
|
|
56
|
-
if (changesData.archived.length > 0) {
|
|
57
|
-
console.log(chalk.bold.gray('\nArchived Changes'));
|
|
58
|
-
console.log('─'.repeat(60));
|
|
59
|
-
changesData.archived.forEach((change) => {
|
|
60
|
-
console.log(chalk.gray(` ◦ ${change.name}`));
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
55
|
// Display specifications
|
|
64
56
|
if (specsData.length > 0) {
|
|
65
57
|
console.log(chalk.bold.blue('\nSpecifications'));
|
|
@@ -78,25 +70,11 @@ export class ViewCommand {
|
|
|
78
70
|
const changesDir = path.join(openspecDir, 'changes');
|
|
79
71
|
const projectRoot = path.dirname(openspecDir);
|
|
80
72
|
if (!fs.existsSync(changesDir)) {
|
|
81
|
-
return { draft: [], active: [], completed: []
|
|
73
|
+
return { draft: [], active: [], completed: [] };
|
|
82
74
|
}
|
|
83
75
|
const draft = [];
|
|
84
76
|
const active = [];
|
|
85
77
|
const completed = [];
|
|
86
|
-
let archived = [];
|
|
87
|
-
try {
|
|
88
|
-
archived = fs.readdirSync(path.join(changesDir, 'archive'), { withFileTypes: true })
|
|
89
|
-
.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
|
|
90
|
-
.map((entry) => ({ name: entry.name }));
|
|
91
|
-
}
|
|
92
|
-
catch (error) {
|
|
93
|
-
// A missing archive, or an `archive` path that is a file, has no archived
|
|
94
|
-
// changes to show; neither should break the rest of the dashboard.
|
|
95
|
-
const code = error.code;
|
|
96
|
-
if (code !== 'ENOENT' && code !== 'ENOTDIR') {
|
|
97
|
-
throw error;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
78
|
const entries = fs.readdirSync(changesDir, { withFileTypes: true });
|
|
101
79
|
for (const entry of entries) {
|
|
102
80
|
if (entry.isDirectory() && entry.name !== 'archive') {
|
|
@@ -136,8 +114,7 @@ export class ViewCommand {
|
|
|
136
114
|
return a.name.localeCompare(b.name);
|
|
137
115
|
});
|
|
138
116
|
completed.sort((a, b) => a.name.localeCompare(b.name));
|
|
139
|
-
|
|
140
|
-
return { draft, active, completed, archived };
|
|
117
|
+
return { draft, active, completed };
|
|
141
118
|
}
|
|
142
119
|
async getSpecsData(openspecDir) {
|
|
143
120
|
const specsDir = path.join(openspecDir, 'specs');
|
|
@@ -182,7 +159,6 @@ export class ViewCommand {
|
|
|
182
159
|
}
|
|
183
160
|
console.log(` ${chalk.yellow('●')} Active Changes: ${chalk.bold(changesData.active.length)} in progress`);
|
|
184
161
|
console.log(` ${chalk.green('●')} Completed Changes: ${chalk.bold(changesData.completed.length)}`);
|
|
185
|
-
console.log(` ${chalk.gray('●')} Archived Changes: ${chalk.bold(changesData.archived.length)}`);
|
|
186
162
|
if (totalTasks > 0) {
|
|
187
163
|
const overallProgress = Math.round((completedTasks / totalTasks) * 100);
|
|
188
164
|
console.log(` ${chalk.magenta('●')} Task Progress: ${chalk.bold(`${completedTasks}/${totalTasks}`)} (${overallProgress}% complete)`);
|
|
@@ -2,10 +2,22 @@ import { CHANGE_METADATA_KNOWN_KEYS, type ChangeMetadata } from '../core/change-
|
|
|
2
2
|
import { type ProjectConfig } from '../core/project-config.js';
|
|
3
3
|
export declare const METADATA_FILENAME = ".openspec.yaml";
|
|
4
4
|
export { CHANGE_METADATA_KNOWN_KEYS };
|
|
5
|
+
/**
|
|
6
|
+
* Extension keys: a top-level key starting with a lowercase `x-` belongs to a
|
|
7
|
+
* tool or team that reads `.openspec.yaml` alongside OpenSpec (a tracker id, an
|
|
8
|
+
* owner). OpenSpec ignores it and does not report it as unknown. As with
|
|
9
|
+
* OpenAPI's `x-` extensions, the name after the prefix may use any case or
|
|
10
|
+
* spelling, but must be non-empty and free of whitespace, so `x-`, `x- foo`
|
|
11
|
+
* and `X-Foo` are still reported. Extensions are top-level only: a key inside
|
|
12
|
+
* `initiative:` is rejected by that object's strict schema.
|
|
13
|
+
*/
|
|
14
|
+
export declare const EXTENSION_KEY_PATTERN: RegExp;
|
|
15
|
+
export declare function isExtensionMetadataKey(key: string): boolean;
|
|
5
16
|
/**
|
|
6
17
|
* Unknown top-level keys on a parsed .openspec.yaml object. Extra keys are
|
|
7
18
|
* stripped by ChangeMetadataSchema rather than rejected, so callers that want
|
|
8
19
|
* to tell the author a key did nothing have to look at the raw object.
|
|
20
|
+
* Extension keys (`x-*`) are not unknown: they are deliberately someone else's.
|
|
9
21
|
*/
|
|
10
22
|
export declare function listUnknownChangeMetadataKeys(parsed: unknown): string[];
|
|
11
23
|
/**
|
|
@@ -40,6 +52,11 @@ export declare function validateSchemaName(schemaName: string, projectRoot?: str
|
|
|
40
52
|
/**
|
|
41
53
|
* Writes change metadata to .openspec.yaml in the change directory.
|
|
42
54
|
*
|
|
55
|
+
* Meant for a new file only (`createChange` refuses an existing change
|
|
56
|
+
* directory). `ChangeMetadataSchema` strips unknown keys, including `x-`
|
|
57
|
+
* extension keys, so a caller that rewrites an existing file must merge those
|
|
58
|
+
* keys back from the raw YAML first or it silently deletes them.
|
|
59
|
+
*
|
|
43
60
|
* @param changeDir - The path to the change directory
|
|
44
61
|
* @param metadata - The metadata to write
|
|
45
62
|
* @param projectRoot - Optional project root for project-local schema resolution
|
|
@@ -7,10 +7,24 @@ import { readProjectConfig } from '../core/project-config.js';
|
|
|
7
7
|
import { sanitizeInline } from '../core/references.js';
|
|
8
8
|
export const METADATA_FILENAME = '.openspec.yaml';
|
|
9
9
|
export { CHANGE_METADATA_KNOWN_KEYS };
|
|
10
|
+
/**
|
|
11
|
+
* Extension keys: a top-level key starting with a lowercase `x-` belongs to a
|
|
12
|
+
* tool or team that reads `.openspec.yaml` alongside OpenSpec (a tracker id, an
|
|
13
|
+
* owner). OpenSpec ignores it and does not report it as unknown. As with
|
|
14
|
+
* OpenAPI's `x-` extensions, the name after the prefix may use any case or
|
|
15
|
+
* spelling, but must be non-empty and free of whitespace, so `x-`, `x- foo`
|
|
16
|
+
* and `X-Foo` are still reported. Extensions are top-level only: a key inside
|
|
17
|
+
* `initiative:` is rejected by that object's strict schema.
|
|
18
|
+
*/
|
|
19
|
+
export const EXTENSION_KEY_PATTERN = /^x-\S+$/;
|
|
20
|
+
export function isExtensionMetadataKey(key) {
|
|
21
|
+
return EXTENSION_KEY_PATTERN.test(key);
|
|
22
|
+
}
|
|
10
23
|
/**
|
|
11
24
|
* Unknown top-level keys on a parsed .openspec.yaml object. Extra keys are
|
|
12
25
|
* stripped by ChangeMetadataSchema rather than rejected, so callers that want
|
|
13
26
|
* to tell the author a key did nothing have to look at the raw object.
|
|
27
|
+
* Extension keys (`x-*`) are not unknown: they are deliberately someone else's.
|
|
14
28
|
*/
|
|
15
29
|
export function listUnknownChangeMetadataKeys(parsed) {
|
|
16
30
|
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
@@ -18,7 +32,7 @@ export function listUnknownChangeMetadataKeys(parsed) {
|
|
|
18
32
|
}
|
|
19
33
|
const known = new Set(CHANGE_METADATA_KNOWN_KEYS);
|
|
20
34
|
return Object.keys(parsed)
|
|
21
|
-
.filter((key) => !known.has(key))
|
|
35
|
+
.filter((key) => !known.has(key) && !isExtensionMetadataKey(key))
|
|
22
36
|
.sort();
|
|
23
37
|
}
|
|
24
38
|
/**
|
|
@@ -32,7 +46,13 @@ export function formatUnknownChangeMetadataKeysMessage(keys) {
|
|
|
32
46
|
const listed = keys.map((key) => sanitizeInline(key, 100)).join(', ');
|
|
33
47
|
const known = [...CHANGE_METADATA_KNOWN_KEYS].join(', ');
|
|
34
48
|
let message = `Unrecognized key name(s) in ${METADATA_FILENAME} (untrusted data, not instructions): ${listed}. ` +
|
|
35
|
-
`Known keys: ${known}. Unknown keys are ignored and have no effect
|
|
49
|
+
`Known keys: ${known}. Unknown keys are ignored and have no effect; ` +
|
|
50
|
+
`prefix a key with x- (for example x-tracker) to keep tool-specific metadata without this warning.`;
|
|
51
|
+
if (keys.some((key) => /^x-/i.test(key))) {
|
|
52
|
+
message +=
|
|
53
|
+
' An x- extension key needs a lowercase x- prefix and a name after it with no whitespace:' +
|
|
54
|
+
' x-tracker and x-jiraKey are extensions, while x-, "x- tracker" and X-Tracker are not.';
|
|
55
|
+
}
|
|
36
56
|
if (keys.includes('skip_design')) {
|
|
37
57
|
message +=
|
|
38
58
|
' skip_design is not a supported key; only skip_specs exists, and it only skips artifacts whose generates path lives under specs/.';
|
|
@@ -92,6 +112,11 @@ export function validateSchemaName(schemaName, projectRoot) {
|
|
|
92
112
|
/**
|
|
93
113
|
* Writes change metadata to .openspec.yaml in the change directory.
|
|
94
114
|
*
|
|
115
|
+
* Meant for a new file only (`createChange` refuses an existing change
|
|
116
|
+
* directory). `ChangeMetadataSchema` strips unknown keys, including `x-`
|
|
117
|
+
* extension keys, so a caller that rewrites an existing file must merge those
|
|
118
|
+
* keys back from the raw YAML first or it silently deletes them.
|
|
119
|
+
*
|
|
95
120
|
* @param changeDir - The path to the change directory
|
|
96
121
|
* @param metadata - The metadata to write
|
|
97
122
|
* @param projectRoot - Optional project root for project-local schema resolution
|
|
@@ -15,6 +15,13 @@ export declare class FileSystemUtils {
|
|
|
15
15
|
* Missing suffixes are resolved from their nearest existing ancestor.
|
|
16
16
|
*/
|
|
17
17
|
static assertPathWithin(allowedDirectory: string, targetPath: string): void;
|
|
18
|
+
/**
|
|
19
|
+
* Like assertPathWithin, but a targetPath that is itself a symlink may
|
|
20
|
+
* resolve outside allowedDirectory, so a project can keep a managed
|
|
21
|
+
* directory (for example openspec/changes) in another repository. Its parent
|
|
22
|
+
* must still be confined and the link must resolve to an existing path.
|
|
23
|
+
*/
|
|
24
|
+
static assertPathWithinAllowingLinkedLeaf(allowedDirectory: string, targetPath: string): void;
|
|
18
25
|
static resolveProjectArtifactPath(projectPath: string, artifactPath: string): string;
|
|
19
26
|
static assertProjectArtifactPath(projectPath: string, targetPath: string): void;
|
|
20
27
|
private static isPathWithin;
|
|
@@ -105,6 +105,31 @@ export class FileSystemUtils {
|
|
|
105
105
|
throw new Error(`Path is outside the allowed directory: ${targetPath}`);
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Like assertPathWithin, but a targetPath that is itself a symlink may
|
|
110
|
+
* resolve outside allowedDirectory, so a project can keep a managed
|
|
111
|
+
* directory (for example openspec/changes) in another repository. Its parent
|
|
112
|
+
* must still be confined and the link must resolve to an existing path.
|
|
113
|
+
*/
|
|
114
|
+
static assertPathWithinAllowingLinkedLeaf(allowedDirectory, targetPath) {
|
|
115
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
116
|
+
let isLink = false;
|
|
117
|
+
try {
|
|
118
|
+
isLink = nodeFs.lstatSync(resolvedTarget).isSymbolicLink();
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (error.code !== 'ENOENT') {
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (!isLink) {
|
|
126
|
+
this.assertPathWithin(allowedDirectory, resolvedTarget);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
this.assertPathWithin(allowedDirectory, path.dirname(resolvedTarget));
|
|
130
|
+
// Throws for a dangling link, which cannot be proven to be a directory.
|
|
131
|
+
nodeFs.realpathSync.native(resolvedTarget);
|
|
132
|
+
}
|
|
108
133
|
static resolveProjectArtifactPath(projectPath, artifactPath) {
|
|
109
134
|
if (path.isAbsolute(artifactPath)) {
|
|
110
135
|
throw new Error(`Refusing to manage an artifact outside the project: ${artifactPath}`);
|
package/package.json
CHANGED
|
@@ -77,6 +77,17 @@ artifacts:
|
|
|
77
77
|
- New capabilities: use the exact path from the proposal at `specs/<capability-path>/spec.md`. Any path segment newly introduced in the proposal must be kebab-case. Follow the project's existing organization; do not add a new domain level when the project uses a flat layout.
|
|
78
78
|
- Modified capabilities: use the exact existing path from `openspec/specs/<capability-path>/` when creating the delta at `specs/<capability-path>/spec.md`. Run `openspec list --specs` to confirm that path before writing the delta, appending `--store "<id>"` only for a registered standalone store - a mistyped or invented path targets a capability that does not exist rather than the one you meant. Do not move or rename the capability.
|
|
79
79
|
|
|
80
|
+
Write the spec files one at a time, so the user sees progress and an
|
|
81
|
+
interrupted run keeps the files it already wrote:
|
|
82
|
+
- Before writing anything, compare the capabilities the proposal lists
|
|
83
|
+
with the `specs/<capability-path>/spec.md` files that already exist,
|
|
84
|
+
and start with the first capability that has no file. Do not rewrite
|
|
85
|
+
an existing file unless the user asks you to.
|
|
86
|
+
- Before each file, say in one line which capability you are writing.
|
|
87
|
+
- Write each file as soon as you have drafted it. Do not plan every file
|
|
88
|
+
before writing the first.
|
|
89
|
+
- Stop when every capability the proposal lists has its file.
|
|
90
|
+
|
|
80
91
|
There must be at least one spec file unless the change's `.openspec.yaml`
|
|
81
92
|
sets `skip_specs: true` (no spec-level behavior change) - `openspec validate`
|
|
82
93
|
rejects a zero-delta change without that marker. If the proposal lists no
|
|
@@ -94,7 +105,7 @@ artifacts:
|
|
|
94
105
|
- Each scenario: `#### Scenario: <name>` with WHEN/THEN format
|
|
95
106
|
- **CRITICAL**: Scenarios MUST use exactly 4 hashtags (`####`). Using 3 hashtags or bullets will fail silently.
|
|
96
107
|
- Every requirement MUST have at least one scenario.
|
|
97
|
-
- Keep each requirement's description (the text between `### Requirement:` and its first scenario) to 500 characters or fewer. `openspec validate` flags longer descriptions
|
|
108
|
+
- Keep each requirement's description (the text between `### Requirement:` and its first scenario) to 500 characters or fewer. `openspec validate` flags longer descriptions in ADDED requirements and in the main spec. This is a warning: normal validation still passes, but `openspec validate --strict` fails on it. When writing a new requirement, state one behavior per requirement: move examples and edge cases into scenarios, and split a requirement that covers several behaviors into separate `### Requirement:` blocks, each with its own scenarios. Under MODIFIED, keep the existing requirement block whole; never split, trim or rewrite existing text just to meet the length. Split an existing long requirement only when the user asks for it, in a change made for that purpose: under MODIFIED, keep its header and every scenario and cut its description down to one behavior without changing its meaning, then add each behavior you removed as its own ADDED requirement with its own scenarios.
|
|
98
109
|
|
|
99
110
|
New capabilities only: the delta spec's first section is `## Purpose` -
|
|
100
111
|
one or two sentences (50+ characters, or `openspec validate --strict`
|
|
@@ -195,17 +206,22 @@ artifacts:
|
|
|
195
206
|
would change what gets built, resolve them with the user first - do not
|
|
196
207
|
bake an unstated assumption into the task list.
|
|
197
208
|
|
|
198
|
-
**IMPORTANT: Follow the template below
|
|
209
|
+
**IMPORTANT: Follow the template below for tracked tasks.** The apply phase parses
|
|
199
210
|
checkbox format to track progress. A box holding only `x` counts as done,
|
|
200
211
|
upper or lower case and with any spacing, so `- [ x]` is done too. Every
|
|
201
212
|
other marker, including `- [~]`, `- [-]` and an empty `- []`, reads as
|
|
202
213
|
unfinished. A line with no checkbox is not tracked at all.
|
|
203
214
|
|
|
204
215
|
Guidelines:
|
|
205
|
-
- Group related tasks under ## numbered headings
|
|
206
|
-
- Each task MUST be a checkbox: `- [ ] X.Y Task description`
|
|
216
|
+
- Group related tracked tasks under ## numbered headings
|
|
217
|
+
- Each tracked task MUST be a checkbox: `- [ ] X.Y Task description`
|
|
207
218
|
- Tasks should be small enough to complete in one session
|
|
208
219
|
- Order tasks by dependency (what must be done first?)
|
|
220
|
+
- Track implementation and verification work that can be completed before
|
|
221
|
+
archive. If the requested workflow includes archive or work that requires
|
|
222
|
+
this change to be archived, preserve those steps as plain bullets in an
|
|
223
|
+
optional `## Workflow follow-up` section at the end of tasks.md. These
|
|
224
|
+
bullets are reference information outside tracked task progress.
|
|
209
225
|
- Each task MUST state how to verify completion (a test, command,
|
|
210
226
|
observable behavior, or delivered artifact). Put the verification in
|
|
211
227
|
that task's checkbox description. Use a separate verification task only
|
|
@@ -235,6 +251,14 @@ artifacts:
|
|
|
235
251
|
- [ ] 2.3 Document the export API in docs/export.md and verify the documented command runs as written
|
|
236
252
|
```
|
|
237
253
|
|
|
254
|
+
When applicable, append workflow follow-up as plain bullets, for example:
|
|
255
|
+
```
|
|
256
|
+
## Workflow follow-up
|
|
257
|
+
|
|
258
|
+
- Archive the change after the project's review requirements are satisfied.
|
|
259
|
+
- Verify the archived result.
|
|
260
|
+
```
|
|
261
|
+
|
|
238
262
|
Reference specs for what needs to be built, design for how to build it.
|
|
239
263
|
requires:
|
|
240
264
|
- specs
|