@fission-ai/openspec 1.4.1 → 1.6.0-beta.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/README.md +37 -4
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +95 -95
- package/dist/commands/change.d.ts +5 -2
- package/dist/commands/change.js +23 -55
- package/dist/commands/config.js +6 -65
- package/dist/commands/context.d.ts +3 -0
- package/dist/commands/context.js +155 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.js +163 -0
- package/dist/commands/shared-gather.d.ts +14 -0
- package/dist/commands/shared-gather.js +31 -0
- package/dist/commands/shared-output.d.ts +18 -0
- package/dist/commands/shared-output.js +61 -0
- package/dist/commands/show.d.ts +11 -6
- package/dist/commands/show.js +70 -25
- package/dist/commands/spec.d.ts +5 -1
- package/dist/commands/spec.js +15 -4
- package/dist/commands/store.d.ts +3 -0
- package/dist/commands/store.js +547 -0
- package/dist/commands/validate.d.ts +10 -0
- package/dist/commands/validate.js +88 -40
- package/dist/commands/workflow/index.d.ts +0 -2
- package/dist/commands/workflow/index.js +0 -1
- package/dist/commands/workflow/instructions.d.ts +11 -1
- package/dist/commands/workflow/instructions.js +71 -22
- package/dist/commands/workflow/new-change.d.ts +6 -3
- package/dist/commands/workflow/new-change.js +42 -77
- package/dist/commands/workflow/shared.d.ts +15 -3
- package/dist/commands/workflow/shared.js +20 -3
- package/dist/commands/workflow/status.d.ts +2 -0
- package/dist/commands/workflow/status.js +19 -17
- package/dist/commands/workset-input.d.ts +19 -0
- package/dist/commands/workset-input.js +112 -0
- package/dist/commands/workset-prompts.d.ts +12 -0
- package/dist/commands/workset-prompts.js +143 -0
- package/dist/commands/workset.d.ts +25 -0
- package/dist/commands/workset.js +446 -0
- package/dist/core/archive.d.ts +17 -6
- package/dist/core/archive.js +220 -68
- package/dist/core/artifact-graph/index.d.ts +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +18 -13
- package/dist/core/artifact-graph/instruction-loader.js +8 -31
- package/dist/core/change-metadata/schema.d.ts +1 -0
- package/dist/core/change-metadata/schema.js +4 -2
- package/dist/core/change-status-policy.d.ts +5 -18
- package/dist/core/change-status-policy.js +2 -37
- package/dist/core/command-generation/adapters/bob.js +1 -14
- package/dist/core/command-generation/adapters/claude.d.ts +1 -1
- package/dist/core/command-generation/adapters/claude.js +4 -15
- package/dist/core/command-generation/adapters/cursor.js +1 -14
- package/dist/core/command-generation/adapters/index.d.ts +2 -0
- package/dist/core/command-generation/adapters/index.js +2 -0
- package/dist/core/command-generation/adapters/oh-my-pi.d.ts +21 -0
- package/dist/core/command-generation/adapters/oh-my-pi.js +44 -0
- package/dist/core/command-generation/adapters/pi.js +1 -14
- package/dist/core/command-generation/adapters/trae.d.ts +13 -0
- package/dist/core/command-generation/adapters/trae.js +48 -0
- package/dist/core/command-generation/adapters/windsurf.js +1 -14
- package/dist/core/command-generation/registry.js +4 -0
- package/dist/core/command-generation/yaml.d.ts +22 -0
- package/dist/core/command-generation/yaml.js +38 -0
- package/dist/core/completions/command-registry.js +87 -289
- package/dist/core/completions/generators/zsh-generator.js +3 -1
- package/dist/core/completions/installers/bash-installer.js +3 -0
- package/dist/core/completions/installers/fish-installer.js +8 -0
- package/dist/core/completions/installers/powershell-installer.js +30 -2
- package/dist/core/completions/installers/zsh-installer.js +3 -0
- package/dist/core/completions/shared-flags.d.ts +1 -0
- package/dist/core/completions/shared-flags.js +5 -0
- package/dist/core/config-schema.d.ts +2 -1
- package/dist/core/config-schema.js +26 -0
- package/dist/core/config.js +1 -0
- package/dist/core/file-state.d.ts +36 -0
- package/dist/core/file-state.js +112 -0
- package/dist/core/global-config.d.ts +2 -0
- package/dist/core/id.d.ts +17 -0
- package/dist/core/id.js +30 -0
- package/dist/core/index.d.ts +3 -3
- package/dist/core/index.js +3 -3
- package/dist/core/init.js +27 -6
- package/dist/core/list.d.ts +2 -0
- package/dist/core/list.js +37 -14
- package/dist/core/openers.d.ts +77 -0
- package/dist/core/openers.js +251 -0
- package/dist/core/openspec-root.d.ts +45 -0
- package/dist/core/openspec-root.js +195 -0
- package/dist/core/parsers/change-parser.js +2 -1
- package/dist/core/parsers/markdown-parser.d.ts +0 -3
- package/dist/core/parsers/markdown-parser.js +5 -59
- package/dist/core/parsers/requirement-blocks.d.ts +11 -0
- package/dist/core/parsers/requirement-blocks.js +42 -8
- package/dist/core/parsers/requirement-text.d.ts +55 -0
- package/dist/core/parsers/requirement-text.js +136 -0
- package/dist/core/planning-home.d.ts +1 -6
- package/dist/core/planning-home.js +2 -43
- package/dist/core/profile-sync-drift.js +1 -0
- package/dist/core/profiles.d.ts +2 -2
- package/dist/core/profiles.js +2 -1
- package/dist/core/project-config.d.ts +44 -1
- package/dist/core/project-config.js +160 -10
- package/dist/core/references.d.ts +63 -0
- package/dist/core/references.js +310 -0
- package/dist/core/relationship-health.d.ts +65 -0
- package/dist/core/relationship-health.js +64 -0
- package/dist/core/root-selection.d.ts +122 -0
- package/dist/core/root-selection.js +337 -0
- package/dist/core/schemas/base.schema.js +7 -2
- package/dist/core/shared/allowed-tools.d.ts +12 -0
- package/dist/core/shared/allowed-tools.js +12 -0
- package/dist/core/shared/skill-generation.js +5 -1
- package/dist/core/shared/tool-detection.d.ts +2 -2
- package/dist/core/shared/tool-detection.js +2 -0
- package/dist/core/specs-apply.d.ts +6 -1
- package/dist/core/specs-apply.js +41 -5
- package/dist/core/store/errors.d.ts +20 -0
- package/dist/core/{context-store → store}/errors.js +3 -3
- package/dist/core/store/foundation.d.ts +56 -0
- package/dist/core/store/foundation.js +251 -0
- package/dist/core/store/git.d.ts +23 -0
- package/dist/core/store/git.js +137 -0
- package/dist/core/{context-store → store}/index.d.ts +0 -1
- package/dist/core/{context-store → store}/index.js +0 -1
- package/dist/core/store/operations.d.ts +114 -0
- package/dist/core/store/operations.js +803 -0
- package/dist/core/store/registry.d.ts +58 -0
- package/dist/core/store/registry.js +275 -0
- package/dist/core/templates/skill-templates.d.ts +1 -0
- package/dist/core/templates/skill-templates.js +1 -0
- package/dist/core/templates/workflows/apply-change.js +5 -4
- package/dist/core/templates/workflows/archive-change.js +5 -4
- package/dist/core/templates/workflows/bulk-archive-change.js +5 -4
- package/dist/core/templates/workflows/continue-change.js +7 -2
- package/dist/core/templates/workflows/explore.js +5 -0
- package/dist/core/templates/workflows/ff-change.js +5 -0
- package/dist/core/templates/workflows/new-change.js +5 -0
- package/dist/core/templates/workflows/onboard.js +4 -1
- package/dist/core/templates/workflows/propose.js +5 -0
- package/dist/core/templates/workflows/store-selection.d.ts +8 -0
- package/dist/core/templates/workflows/store-selection.js +8 -0
- package/dist/core/templates/workflows/sync-specs.js +5 -4
- package/dist/core/templates/workflows/update-change.d.ts +10 -0
- package/dist/core/templates/workflows/update-change.js +167 -0
- package/dist/core/templates/workflows/verify-change.js +5 -4
- package/dist/core/update.js +4 -4
- package/dist/core/validation/validator.d.ts +7 -0
- package/dist/core/validation/validator.js +101 -38
- package/dist/core/view.js +1 -1
- package/dist/core/working-set.d.ts +47 -0
- package/dist/core/working-set.js +43 -0
- package/dist/core/worksets.d.ts +75 -0
- package/dist/core/worksets.js +245 -0
- package/dist/core/zod-issues.d.ts +4 -0
- package/dist/core/zod-issues.js +10 -0
- package/dist/utils/change-metadata.d.ts +1 -0
- package/dist/utils/change-metadata.js +1 -1
- package/dist/utils/change-utils.js +15 -0
- package/dist/utils/file-system.js +36 -17
- package/dist/utils/task-progress.d.ts +10 -1
- package/dist/utils/task-progress.js +66 -2
- package/package.json +26 -23
- package/dist/commands/context-store.d.ts +0 -3
- package/dist/commands/context-store.js +0 -475
- package/dist/commands/initiative.d.ts +0 -13
- package/dist/commands/initiative.js +0 -318
- package/dist/commands/workflow/initiative-link.d.ts +0 -24
- package/dist/commands/workflow/initiative-link.js +0 -47
- package/dist/commands/workflow/set-change.d.ts +0 -13
- package/dist/commands/workflow/set-change.js +0 -87
- package/dist/commands/workspace/context-status.d.ts +0 -4
- package/dist/commands/workspace/context-status.js +0 -59
- package/dist/commands/workspace/open-target-selection.d.ts +0 -13
- package/dist/commands/workspace/open-target-selection.js +0 -146
- package/dist/commands/workspace/open-view.d.ts +0 -62
- package/dist/commands/workspace/open-view.js +0 -249
- package/dist/commands/workspace/open.d.ts +0 -37
- package/dist/commands/workspace/open.js +0 -110
- package/dist/commands/workspace/opener-selection.d.ts +0 -11
- package/dist/commands/workspace/opener-selection.js +0 -98
- package/dist/commands/workspace/operations.d.ts +0 -29
- package/dist/commands/workspace/operations.js +0 -543
- package/dist/commands/workspace/prompt-theme.d.ts +0 -29
- package/dist/commands/workspace/prompt-theme.js +0 -24
- package/dist/commands/workspace/registration.d.ts +0 -13
- package/dist/commands/workspace/registration.js +0 -84
- package/dist/commands/workspace/selection.d.ts +0 -8
- package/dist/commands/workspace/selection.js +0 -104
- package/dist/commands/workspace/setup-prompts.d.ts +0 -13
- package/dist/commands/workspace/setup-prompts.js +0 -121
- package/dist/commands/workspace/types.d.ts +0 -103
- package/dist/commands/workspace/types.js +0 -36
- package/dist/commands/workspace.d.ts +0 -5
- package/dist/commands/workspace.js +0 -574
- package/dist/core/collections/index.d.ts +0 -3
- package/dist/core/collections/index.js +0 -3
- package/dist/core/collections/initiatives/collection.d.ts +0 -4
- package/dist/core/collections/initiatives/collection.js +0 -17
- package/dist/core/collections/initiatives/index.d.ts +0 -6
- package/dist/core/collections/initiatives/index.js +0 -6
- package/dist/core/collections/initiatives/operations.d.ts +0 -49
- package/dist/core/collections/initiatives/operations.js +0 -175
- package/dist/core/collections/initiatives/resolution.d.ts +0 -87
- package/dist/core/collections/initiatives/resolution.js +0 -374
- package/dist/core/collections/initiatives/schema.d.ts +0 -41
- package/dist/core/collections/initiatives/schema.js +0 -134
- package/dist/core/collections/initiatives/templates.d.ts +0 -12
- package/dist/core/collections/initiatives/templates.js +0 -90
- package/dist/core/collections/runtime.d.ts +0 -46
- package/dist/core/collections/runtime.js +0 -194
- package/dist/core/context-store/binding.d.ts +0 -53
- package/dist/core/context-store/binding.js +0 -197
- package/dist/core/context-store/errors.d.ts +0 -20
- package/dist/core/context-store/foundation.d.ts +0 -55
- package/dist/core/context-store/foundation.js +0 -321
- package/dist/core/context-store/operations.d.ts +0 -85
- package/dist/core/context-store/operations.js +0 -528
- package/dist/core/context-store/registry.d.ts +0 -45
- package/dist/core/context-store/registry.js +0 -229
- package/dist/core/workspace/foundation.d.ts +0 -67
- package/dist/core/workspace/foundation.js +0 -295
- package/dist/core/workspace/index.d.ts +0 -8
- package/dist/core/workspace/index.js +0 -8
- package/dist/core/workspace/legacy-state.d.ts +0 -28
- package/dist/core/workspace/legacy-state.js +0 -200
- package/dist/core/workspace/link-input.d.ts +0 -9
- package/dist/core/workspace/link-input.js +0 -32
- package/dist/core/workspace/open-surface.d.ts +0 -45
- package/dist/core/workspace/open-surface.js +0 -215
- package/dist/core/workspace/openers.d.ts +0 -21
- package/dist/core/workspace/openers.js +0 -124
- package/dist/core/workspace/registry.d.ts +0 -24
- package/dist/core/workspace/registry.js +0 -146
- package/dist/core/workspace/skills.d.ts +0 -57
- package/dist/core/workspace/skills.js +0 -334
- package/dist/core/workspace/state-io.d.ts +0 -10
- package/dist/core/workspace/state-io.js +0 -121
- package/schemas/workspace-planning/schema.yaml +0 -72
- package/schemas/workspace-planning/templates/design.md +0 -33
- package/schemas/workspace-planning/templates/proposal.md +0 -28
- package/schemas/workspace-planning/templates/spec.md +0 -9
- package/schemas/workspace-planning/templates/tasks.md +0 -15
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { buildCodeFenceMask } from './requirement-text.js';
|
|
1
2
|
export function normalizeRequirementName(name) {
|
|
2
3
|
return name.trim();
|
|
3
4
|
}
|
|
5
|
+
/** The canonical requirement header the delta reader recognizes. */
|
|
4
6
|
const REQUIREMENT_HEADER_REGEX = /^###\s*Requirement:\s*(.+)\s*$/i;
|
|
5
7
|
/**
|
|
6
8
|
* Extracts the Requirements section from a spec file and parses requirement blocks.
|
|
@@ -84,15 +86,26 @@ export function parseDeltaSpec(content) {
|
|
|
84
86
|
const modifiedLookup = getSectionCaseInsensitive(sections, 'MODIFIED Requirements');
|
|
85
87
|
const removedLookup = getSectionCaseInsensitive(sections, 'REMOVED Requirements');
|
|
86
88
|
const renamedLookup = getSectionCaseInsensitive(sections, 'RENAMED Requirements');
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
+
const skippedHeaders = [];
|
|
90
|
+
const added = parseRequirementBlocksFromSection(addedLookup.body, {
|
|
91
|
+
section: addedLookup.title,
|
|
92
|
+
bodyStartLine: addedLookup.bodyStartLine,
|
|
93
|
+
sink: skippedHeaders,
|
|
94
|
+
});
|
|
95
|
+
const modified = parseRequirementBlocksFromSection(modifiedLookup.body, {
|
|
96
|
+
section: modifiedLookup.title,
|
|
97
|
+
bodyStartLine: modifiedLookup.bodyStartLine,
|
|
98
|
+
sink: skippedHeaders,
|
|
99
|
+
});
|
|
89
100
|
const removedNames = parseRemovedNames(removedLookup.body);
|
|
90
101
|
const renamedPairs = parseRenamedPairs(renamedLookup.body);
|
|
102
|
+
skippedHeaders.sort((a, b) => a.line - b.line);
|
|
91
103
|
return {
|
|
92
104
|
added,
|
|
93
105
|
modified,
|
|
94
106
|
removed: removedNames,
|
|
95
107
|
renamed: renamedPairs,
|
|
108
|
+
skippedHeaders,
|
|
96
109
|
sectionPresence: {
|
|
97
110
|
added: addedLookup.found,
|
|
98
111
|
modified: modifiedLookup.found,
|
|
@@ -116,28 +129,48 @@ function splitTopLevelSections(content) {
|
|
|
116
129
|
const current = indices[i];
|
|
117
130
|
const next = indices[i + 1];
|
|
118
131
|
const body = lines.slice(current.index + 1, next ? next.index : lines.length).join('\n');
|
|
119
|
-
|
|
132
|
+
// First body line, 1-based: the header is at 0-based current.index.
|
|
133
|
+
result[current.title] = { body, bodyStartLine: current.index + 2 };
|
|
120
134
|
}
|
|
121
135
|
return result;
|
|
122
136
|
}
|
|
123
137
|
function getSectionCaseInsensitive(sections, desired) {
|
|
124
138
|
const target = desired.toLowerCase();
|
|
125
|
-
for (const [title, body] of Object.entries(sections)) {
|
|
139
|
+
for (const [title, { body, bodyStartLine }] of Object.entries(sections)) {
|
|
126
140
|
if (title.toLowerCase() === target)
|
|
127
|
-
return { body, found: true };
|
|
141
|
+
return { title, body, bodyStartLine, found: true };
|
|
128
142
|
}
|
|
129
|
-
return { body: '', found: false };
|
|
143
|
+
return { title: desired, body: '', bodyStartLine: 0, found: false };
|
|
130
144
|
}
|
|
131
|
-
function parseRequirementBlocksFromSection(sectionBody) {
|
|
145
|
+
function parseRequirementBlocksFromSection(sectionBody, skipped) {
|
|
132
146
|
if (!sectionBody)
|
|
133
147
|
return [];
|
|
134
148
|
const lines = normalizeLineEndings(sectionBody).split('\n');
|
|
149
|
+
// Record the non-canonical level-3 headers this reader skips, at the moment
|
|
150
|
+
// it skips them, so the INFO note describes the reader's real boundaries.
|
|
151
|
+
// Fence-masked lines are excluded: the body reader treats them as fenced
|
|
152
|
+
// content, not as headers.
|
|
153
|
+
const fenceMask = skipped ? buildCodeFenceMask(lines) : undefined;
|
|
154
|
+
const recordIfSkippedHeader = (index) => {
|
|
155
|
+
if (!skipped || fenceMask[index])
|
|
156
|
+
return;
|
|
157
|
+
const h3 = lines[index].match(/^###\s+(.+?)\s*$/);
|
|
158
|
+
if (h3 && !REQUIREMENT_HEADER_REGEX.test(lines[index])) {
|
|
159
|
+
skipped.sink.push({
|
|
160
|
+
header: h3[1].trim(),
|
|
161
|
+
section: skipped.section,
|
|
162
|
+
line: skipped.bodyStartLine + index,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
};
|
|
135
166
|
const blocks = [];
|
|
136
167
|
let i = 0;
|
|
137
168
|
while (i < lines.length) {
|
|
138
169
|
// Seek next requirement header
|
|
139
|
-
while (i < lines.length && !REQUIREMENT_HEADER_REGEX.test(lines[i]))
|
|
170
|
+
while (i < lines.length && !REQUIREMENT_HEADER_REGEX.test(lines[i])) {
|
|
171
|
+
recordIfSkippedHeader(i);
|
|
140
172
|
i++;
|
|
173
|
+
}
|
|
141
174
|
if (i >= lines.length)
|
|
142
175
|
break;
|
|
143
176
|
const headerLine = lines[i];
|
|
@@ -150,6 +183,7 @@ function parseRequirementBlocksFromSection(sectionBody) {
|
|
|
150
183
|
const buf = [headerLine];
|
|
151
184
|
i++;
|
|
152
185
|
while (i < lines.length && !REQUIREMENT_HEADER_REGEX.test(lines[i]) && !/^##\s+/.test(lines[i])) {
|
|
186
|
+
recordIfSkippedHeader(i);
|
|
153
187
|
buf.push(lines[i]);
|
|
154
188
|
i++;
|
|
155
189
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared, fence-aware requirement-reading helpers.
|
|
3
|
+
*
|
|
4
|
+
* The requirement reader used to be implemented twice — once for main specs
|
|
5
|
+
* (`MarkdownParser.parseRequirements`) and once for change deltas
|
|
6
|
+
* (`Validator.extractRequirementText` / `countScenarios`) — and the two drifted
|
|
7
|
+
* apart. These helpers are the single source of truth for requirement-body
|
|
8
|
+
* extraction, scenario counting, and `SHALL`/`MUST` detection in
|
|
9
|
+
* `validate <change>`, `validate <spec>`, and `archive`.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Build a per-line mask marking lines that fall inside a fenced code block
|
|
13
|
+
* (``` ``` ``` or ``` ~~~ ```), including the fence lines themselves. Mirrors the
|
|
14
|
+
* fence rules markdown uses: a fence opens on the first ```` ```/~~~ ```` of
|
|
15
|
+
* length >= 3 and closes on a line of the same marker whose length is >= the
|
|
16
|
+
* opening length, with nothing but whitespace after it.
|
|
17
|
+
*/
|
|
18
|
+
export declare function buildCodeFenceMask(lines: string[]): boolean[];
|
|
19
|
+
/**
|
|
20
|
+
* The one predicate for normative-keyword detection. Matches `SHALL` or `MUST`
|
|
21
|
+
* as whole words so the change-delta reader and the schema-based reader accept
|
|
22
|
+
* and reject identical text.
|
|
23
|
+
*/
|
|
24
|
+
export declare function containsShallOrMust(text: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Extract the full requirement body from the lines that follow a
|
|
27
|
+
* `### Requirement:` header (the lines may include scenarios and fenced code).
|
|
28
|
+
*
|
|
29
|
+
* Captures every body line from the start up to the first header found on a
|
|
30
|
+
* non-fenced line — usually the first `#### Scenario:`, but also a stray `###`
|
|
31
|
+
* divider the delta reader absorbed into the block — skipping blank lines and
|
|
32
|
+
* any line inside a fenced code block. `**metadata**:` lines are skipped only
|
|
33
|
+
* when other body text remains: a requirement written entirely as
|
|
34
|
+
* `**Constraint**: The system MUST ...` keeps that line as its body. Captured
|
|
35
|
+
* lines are trimmed and joined with newlines so a requirement whose text wraps
|
|
36
|
+
* across lines — or whose `SHALL`/`MUST` lands on a later line — is read in
|
|
37
|
+
* full.
|
|
38
|
+
*/
|
|
39
|
+
export declare function extractRequirementBody(bodyLines: string[]): string;
|
|
40
|
+
/**
|
|
41
|
+
* Parser/display fallback for a requirement block with no body text. This is
|
|
42
|
+
* what lets a bare `### The system SHALL ...` header remain readable on the
|
|
43
|
+
* spec path (the title is the requirement). Validator body-keyword checks for
|
|
44
|
+
* canonical `### Requirement:` blocks use `extractRequirementBody` directly so
|
|
45
|
+
* a keyword that appears only in the header still receives the #1156/#1280
|
|
46
|
+
* body-keyword hint.
|
|
47
|
+
*/
|
|
48
|
+
export declare function extractRequirementText(headerTitle: string, bodyLines: string[]): string;
|
|
49
|
+
/**
|
|
50
|
+
* Count the real scenarios in a requirement block: `#### ` headers on non-fenced
|
|
51
|
+
* lines. A `#### Scenario:` that lives inside a fenced example is not a real
|
|
52
|
+
* scenario and is not counted.
|
|
53
|
+
*/
|
|
54
|
+
export declare function countScenarios(bodyLines: string[]): number;
|
|
55
|
+
//# sourceMappingURL=requirement-text.d.ts.map
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared, fence-aware requirement-reading helpers.
|
|
3
|
+
*
|
|
4
|
+
* The requirement reader used to be implemented twice — once for main specs
|
|
5
|
+
* (`MarkdownParser.parseRequirements`) and once for change deltas
|
|
6
|
+
* (`Validator.extractRequirementText` / `countScenarios`) — and the two drifted
|
|
7
|
+
* apart. These helpers are the single source of truth for requirement-body
|
|
8
|
+
* extraction, scenario counting, and `SHALL`/`MUST` detection in
|
|
9
|
+
* `validate <change>`, `validate <spec>`, and `archive`.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Build a per-line mask marking lines that fall inside a fenced code block
|
|
13
|
+
* (``` ``` ``` or ``` ~~~ ```), including the fence lines themselves. Mirrors the
|
|
14
|
+
* fence rules markdown uses: a fence opens on the first ```` ```/~~~ ```` of
|
|
15
|
+
* length >= 3 and closes on a line of the same marker whose length is >= the
|
|
16
|
+
* opening length, with nothing but whitespace after it.
|
|
17
|
+
*/
|
|
18
|
+
export function buildCodeFenceMask(lines) {
|
|
19
|
+
const mask = new Array(lines.length).fill(false);
|
|
20
|
+
let activeFence = null;
|
|
21
|
+
for (let i = 0; i < lines.length; i++) {
|
|
22
|
+
const fence = getFenceMarker(lines[i]);
|
|
23
|
+
if (!activeFence) {
|
|
24
|
+
if (fence) {
|
|
25
|
+
activeFence = fence;
|
|
26
|
+
mask[i] = true;
|
|
27
|
+
}
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
mask[i] = true;
|
|
31
|
+
if (isClosingFence(lines[i], activeFence)) {
|
|
32
|
+
activeFence = null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return mask;
|
|
36
|
+
}
|
|
37
|
+
function getFenceMarker(line) {
|
|
38
|
+
const fenceMatch = line.match(/^\s*(`{3,}|~{3,})/);
|
|
39
|
+
if (!fenceMatch) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
marker: fenceMatch[1][0],
|
|
44
|
+
length: fenceMatch[1].length,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function isClosingFence(line, activeFence) {
|
|
48
|
+
const fenceMatch = line.match(/^\s*(`{3,}|~{3,})\s*$/);
|
|
49
|
+
return Boolean(fenceMatch &&
|
|
50
|
+
fenceMatch[1][0] === activeFence.marker &&
|
|
51
|
+
fenceMatch[1].length >= activeFence.length);
|
|
52
|
+
}
|
|
53
|
+
/** Lines that look like `**ID**: ...` / `**Priority**: ...` metadata. */
|
|
54
|
+
const METADATA_LINE = /^\*\*[^*]+\*\*:/;
|
|
55
|
+
/** Any markdown header line — the boundary where a requirement body ends. */
|
|
56
|
+
const HEADER_LINE = /^#{1,6}\s/;
|
|
57
|
+
/**
|
|
58
|
+
* A level-4 header. Deliberately matches ANY `####` header, not only
|
|
59
|
+
* `#### Scenario:` — the spec path treats every level-4 child of a requirement
|
|
60
|
+
* as a scenario, so the delta counter must too (parity). Don't tighten this to
|
|
61
|
+
* `Scenario:` without changing both paths together.
|
|
62
|
+
*/
|
|
63
|
+
const SCENARIO_HEADER = /^####\s+/;
|
|
64
|
+
/**
|
|
65
|
+
* The one predicate for normative-keyword detection. Matches `SHALL` or `MUST`
|
|
66
|
+
* as whole words so the change-delta reader and the schema-based reader accept
|
|
67
|
+
* and reject identical text.
|
|
68
|
+
*/
|
|
69
|
+
export function containsShallOrMust(text) {
|
|
70
|
+
return /\b(SHALL|MUST)\b/.test(text);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Extract the full requirement body from the lines that follow a
|
|
74
|
+
* `### Requirement:` header (the lines may include scenarios and fenced code).
|
|
75
|
+
*
|
|
76
|
+
* Captures every body line from the start up to the first header found on a
|
|
77
|
+
* non-fenced line — usually the first `#### Scenario:`, but also a stray `###`
|
|
78
|
+
* divider the delta reader absorbed into the block — skipping blank lines and
|
|
79
|
+
* any line inside a fenced code block. `**metadata**:` lines are skipped only
|
|
80
|
+
* when other body text remains: a requirement written entirely as
|
|
81
|
+
* `**Constraint**: The system MUST ...` keeps that line as its body. Captured
|
|
82
|
+
* lines are trimmed and joined with newlines so a requirement whose text wraps
|
|
83
|
+
* across lines — or whose `SHALL`/`MUST` lands on a later line — is read in
|
|
84
|
+
* full.
|
|
85
|
+
*/
|
|
86
|
+
export function extractRequirementBody(bodyLines) {
|
|
87
|
+
const mask = buildCodeFenceMask(bodyLines);
|
|
88
|
+
const captured = [];
|
|
89
|
+
const metadata = [];
|
|
90
|
+
for (let i = 0; i < bodyLines.length; i++) {
|
|
91
|
+
if (mask[i])
|
|
92
|
+
continue; // inside a fenced code block
|
|
93
|
+
const line = bodyLines[i];
|
|
94
|
+
if (HEADER_LINE.test(line))
|
|
95
|
+
break; // first scenario or stray divider
|
|
96
|
+
const trimmed = line.trim();
|
|
97
|
+
if (trimmed.length === 0)
|
|
98
|
+
continue; // blank
|
|
99
|
+
if (METADATA_LINE.test(trimmed)) {
|
|
100
|
+
metadata.push(trimmed); // **ID**: / **Priority**: ...
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
captured.push(trimmed);
|
|
104
|
+
}
|
|
105
|
+
if (captured.length > 0)
|
|
106
|
+
return captured.join('\n');
|
|
107
|
+
return metadata.join('\n'); // metadata-only body: the metadata IS the body
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Parser/display fallback for a requirement block with no body text. This is
|
|
111
|
+
* what lets a bare `### The system SHALL ...` header remain readable on the
|
|
112
|
+
* spec path (the title is the requirement). Validator body-keyword checks for
|
|
113
|
+
* canonical `### Requirement:` blocks use `extractRequirementBody` directly so
|
|
114
|
+
* a keyword that appears only in the header still receives the #1156/#1280
|
|
115
|
+
* body-keyword hint.
|
|
116
|
+
*/
|
|
117
|
+
export function extractRequirementText(headerTitle, bodyLines) {
|
|
118
|
+
return extractRequirementBody(bodyLines) || headerTitle.trim();
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Count the real scenarios in a requirement block: `#### ` headers on non-fenced
|
|
122
|
+
* lines. A `#### Scenario:` that lives inside a fenced example is not a real
|
|
123
|
+
* scenario and is not counted.
|
|
124
|
+
*/
|
|
125
|
+
export function countScenarios(bodyLines) {
|
|
126
|
+
const mask = buildCodeFenceMask(bodyLines);
|
|
127
|
+
let count = 0;
|
|
128
|
+
for (let i = 0; i < bodyLines.length; i++) {
|
|
129
|
+
if (mask[i])
|
|
130
|
+
continue;
|
|
131
|
+
if (SCENARIO_HEADER.test(bodyLines[i]))
|
|
132
|
+
count++;
|
|
133
|
+
}
|
|
134
|
+
return count;
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=requirement-text.js.map
|
|
@@ -1,19 +1,14 @@
|
|
|
1
|
-
export type PlanningHomeKind = 'repo'
|
|
1
|
+
export type PlanningHomeKind = 'repo';
|
|
2
2
|
export interface PlanningHome {
|
|
3
3
|
kind: PlanningHomeKind;
|
|
4
4
|
root: string;
|
|
5
5
|
changesDir: string;
|
|
6
6
|
defaultSchema: string;
|
|
7
|
-
workspace?: {
|
|
8
|
-
name: string;
|
|
9
|
-
links: string[];
|
|
10
|
-
};
|
|
11
7
|
}
|
|
12
8
|
export interface ResolvePlanningHomeOptions {
|
|
13
9
|
startPath?: string;
|
|
14
10
|
allowImplicitRepoRoot?: boolean;
|
|
15
11
|
}
|
|
16
|
-
export declare function findWorkspacePlanningRootSync(startPath?: string): string | null;
|
|
17
12
|
export declare function findRepoPlanningRootSync(startPath?: string): string | null;
|
|
18
13
|
export declare function resolveCurrentPlanningHomeSync(options?: ResolvePlanningHomeOptions): PlanningHome;
|
|
19
14
|
export declare function getChangeDir(planningHome: PlanningHome, changeName: string): string;
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import { getWorkspaceChangesDir, readWorkspaceViewStateSync, workspaceStateFileExistsSync, } from './workspace/index.js';
|
|
4
3
|
import { FileSystemUtils } from '../utils/file-system.js';
|
|
5
4
|
const REPO_DEFAULT_SCHEMA = 'spec-driven';
|
|
6
|
-
const WORKSPACE_DEFAULT_SCHEMA = 'workspace-planning';
|
|
7
5
|
function pathExistsAsDirectory(candidatePath) {
|
|
8
6
|
try {
|
|
9
7
|
return fs.statSync(candidatePath).isDirectory();
|
|
@@ -36,41 +34,9 @@ function findNearestAncestor(startPath, predicate) {
|
|
|
36
34
|
currentDir = parentDir;
|
|
37
35
|
}
|
|
38
36
|
}
|
|
39
|
-
export function findWorkspacePlanningRootSync(startPath = process.cwd()) {
|
|
40
|
-
return findNearestAncestor(startPath, workspaceStateFileExistsSync);
|
|
41
|
-
}
|
|
42
37
|
export function findRepoPlanningRootSync(startPath = process.cwd()) {
|
|
43
38
|
return findNearestAncestor(startPath, (dirPath) => pathExistsAsDirectory(path.join(dirPath, 'openspec')));
|
|
44
39
|
}
|
|
45
|
-
function isSameOrDescendant(rootPath, candidatePath) {
|
|
46
|
-
const relative = path.relative(rootPath, candidatePath);
|
|
47
|
-
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
48
|
-
}
|
|
49
|
-
function countPathSegments(candidatePath) {
|
|
50
|
-
return path.resolve(candidatePath).split(path.sep).filter(Boolean).length;
|
|
51
|
-
}
|
|
52
|
-
function isWindowsLikePath(candidatePath) {
|
|
53
|
-
return /^[A-Za-z]:[\\/]/.test(candidatePath) || candidatePath.startsWith('\\\\');
|
|
54
|
-
}
|
|
55
|
-
function relativePlanningPath(fromPath, toPath) {
|
|
56
|
-
if (isWindowsLikePath(fromPath) || isWindowsLikePath(toPath)) {
|
|
57
|
-
return path.win32.relative(path.win32.normalize(fromPath), path.win32.normalize(toPath));
|
|
58
|
-
}
|
|
59
|
-
return path.posix.relative(fromPath.replace(/\\/g, '/'), toPath.replace(/\\/g, '/'));
|
|
60
|
-
}
|
|
61
|
-
function workspacePlanningHome(workspaceRoot) {
|
|
62
|
-
const viewState = readWorkspaceViewStateSync(workspaceRoot);
|
|
63
|
-
return {
|
|
64
|
-
kind: 'workspace',
|
|
65
|
-
root: workspaceRoot,
|
|
66
|
-
changesDir: getWorkspaceChangesDir(workspaceRoot),
|
|
67
|
-
defaultSchema: WORKSPACE_DEFAULT_SCHEMA,
|
|
68
|
-
workspace: {
|
|
69
|
-
name: viewState?.name ?? path.basename(workspaceRoot),
|
|
70
|
-
links: Object.keys(viewState?.links ?? {}).sort((a, b) => a.localeCompare(b)),
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
40
|
function repoPlanningHome(repoRoot) {
|
|
75
41
|
return {
|
|
76
42
|
kind: 'repo',
|
|
@@ -82,13 +48,7 @@ function repoPlanningHome(repoRoot) {
|
|
|
82
48
|
export function resolveCurrentPlanningHomeSync(options = {}) {
|
|
83
49
|
const startPath = options.startPath ?? process.cwd();
|
|
84
50
|
const searchStart = getSearchStartDirectory(startPath);
|
|
85
|
-
const workspaceRoot = findWorkspacePlanningRootSync(searchStart);
|
|
86
51
|
const repoRoot = findRepoPlanningRootSync(searchStart);
|
|
87
|
-
if (workspaceRoot && isSameOrDescendant(workspaceRoot, searchStart)) {
|
|
88
|
-
if (!repoRoot || countPathSegments(workspaceRoot) >= countPathSegments(repoRoot)) {
|
|
89
|
-
return workspacePlanningHome(workspaceRoot);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
52
|
if (repoRoot) {
|
|
93
53
|
return repoPlanningHome(repoRoot);
|
|
94
54
|
}
|
|
@@ -101,8 +61,7 @@ export function getChangeDir(planningHome, changeName) {
|
|
|
101
61
|
return FileSystemUtils.joinPath(planningHome.changesDir, changeName);
|
|
102
62
|
}
|
|
103
63
|
export function formatChangeLocation(planningHome, changeName) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return relative.length > 0 ? relative : changeDir;
|
|
64
|
+
// Repo homes always nest changesDir under the root.
|
|
65
|
+
return path.relative(planningHome.root, getChangeDir(planningHome, changeName));
|
|
107
66
|
}
|
|
108
67
|
//# sourceMappingURL=planning-home.js.map
|
|
@@ -12,6 +12,7 @@ export const WORKFLOW_TO_SKILL_DIR = {
|
|
|
12
12
|
'new': 'openspec-new-change',
|
|
13
13
|
'continue': 'openspec-continue-change',
|
|
14
14
|
'apply': 'openspec-apply-change',
|
|
15
|
+
'update': 'openspec-update-change',
|
|
15
16
|
'ff': 'openspec-ff-change',
|
|
16
17
|
'sync': 'openspec-sync-specs',
|
|
17
18
|
'archive': 'openspec-archive-change',
|
package/dist/core/profiles.d.ts
CHANGED
|
@@ -9,11 +9,11 @@ import type { Profile } from './global-config.js';
|
|
|
9
9
|
* Core workflows included in the 'core' profile.
|
|
10
10
|
* These provide the streamlined experience for new users.
|
|
11
11
|
*/
|
|
12
|
-
export declare const CORE_WORKFLOWS: readonly ["propose", "explore", "apply", "sync", "archive"];
|
|
12
|
+
export declare const CORE_WORKFLOWS: readonly ["propose", "explore", "apply", "update", "sync", "archive"];
|
|
13
13
|
/**
|
|
14
14
|
* All available workflows in the system.
|
|
15
15
|
*/
|
|
16
|
-
export declare const ALL_WORKFLOWS: readonly ["propose", "explore", "new", "continue", "apply", "ff", "sync", "archive", "bulk-archive", "verify", "onboard"];
|
|
16
|
+
export declare const ALL_WORKFLOWS: readonly ["propose", "explore", "new", "continue", "apply", "update", "ff", "sync", "archive", "bulk-archive", "verify", "onboard"];
|
|
17
17
|
export type WorkflowId = (typeof ALL_WORKFLOWS)[number];
|
|
18
18
|
export type CoreWorkflowId = (typeof CORE_WORKFLOWS)[number];
|
|
19
19
|
/**
|
package/dist/core/profiles.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Core workflows included in the 'core' profile.
|
|
9
9
|
* These provide the streamlined experience for new users.
|
|
10
10
|
*/
|
|
11
|
-
export const CORE_WORKFLOWS = ['propose', 'explore', 'apply', 'sync', 'archive'];
|
|
11
|
+
export const CORE_WORKFLOWS = ['propose', 'explore', 'apply', 'update', 'sync', 'archive'];
|
|
12
12
|
/**
|
|
13
13
|
* All available workflows in the system.
|
|
14
14
|
*/
|
|
@@ -18,6 +18,7 @@ export const ALL_WORKFLOWS = [
|
|
|
18
18
|
'new',
|
|
19
19
|
'continue',
|
|
20
20
|
'apply',
|
|
21
|
+
'update',
|
|
21
22
|
'ff',
|
|
22
23
|
'sync',
|
|
23
24
|
'archive',
|
|
@@ -16,8 +16,18 @@ export declare const ProjectConfigSchema: z.ZodObject<{
|
|
|
16
16
|
schema: z.ZodString;
|
|
17
17
|
context: z.ZodOptional<z.ZodString>;
|
|
18
18
|
rules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
|
|
19
|
+
store: z.ZodOptional<z.ZodString>;
|
|
19
20
|
}, z.core.$strip>;
|
|
20
|
-
|
|
21
|
+
/** Normalized in-memory shape of a referenced store declaration. */
|
|
22
|
+
export interface DeclarationEntry {
|
|
23
|
+
id: string;
|
|
24
|
+
/** Clone source rendered into onboarding fixes. */
|
|
25
|
+
remote?: string;
|
|
26
|
+
}
|
|
27
|
+
export type ProjectConfig = z.infer<typeof ProjectConfigSchema> & {
|
|
28
|
+
references?: DeclarationEntry[];
|
|
29
|
+
};
|
|
30
|
+
export declare const MAX_CONTEXT_SIZE: number;
|
|
21
31
|
/**
|
|
22
32
|
* Read and parse openspec/config.yaml from project root.
|
|
23
33
|
* Uses resilient parsing - validates each field independently using Zod safeParse.
|
|
@@ -61,4 +71,37 @@ export declare function suggestSchemas(invalidSchemaName: string, availableSchem
|
|
|
61
71
|
name: string;
|
|
62
72
|
isBuiltIn: boolean;
|
|
63
73
|
}[]): string;
|
|
74
|
+
export interface StorePointerRead {
|
|
75
|
+
/** The declared store id, when present and a string. */
|
|
76
|
+
value?: string;
|
|
77
|
+
/** Set when the pointer cannot be trusted: the config file could not be
|
|
78
|
+
* read as YAML, or the store key is present but not a string. An empty
|
|
79
|
+
* or comments-only config is NOT malformed - it simply has no pointer. */
|
|
80
|
+
malformed?: 'unparseable' | 'non_string';
|
|
81
|
+
/** Absolute path of the config file actually read, or null when none exists. */
|
|
82
|
+
filePath: string | null;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Warning-silent targeted read of the `store:` pointer. Used by root
|
|
86
|
+
* resolution (which must not re-emit the resilient parser's field
|
|
87
|
+
* warnings) and by `openspec init`'s pointer guard. Unlike
|
|
88
|
+
* `readProjectConfig`, a malformed value is REPORTED, not dropped —
|
|
89
|
+
* a dropped pointer would silently flip where work lands.
|
|
90
|
+
*/
|
|
91
|
+
export declare function readStorePointer(projectRoot: string): StorePointerRead;
|
|
92
|
+
/** Shared .yaml/.yml probe used by readProjectConfig and readStorePointer. */
|
|
93
|
+
export declare function resolveConfigFilePath(projectRoot: string): string | null;
|
|
94
|
+
/** Human rendering of a malformed pointer reason, shared by every surface. */
|
|
95
|
+
export declare function storePointerProblem(reason: 'unparseable' | 'non_string'): string;
|
|
96
|
+
export interface OpenSpecDirClassification {
|
|
97
|
+
/** True when openspec/specs or openspec/changes exists as a directory. */
|
|
98
|
+
hasPlanningShape: boolean;
|
|
99
|
+
pointer: StorePointerRead;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* One classification for "real root vs config-only pointer dir", shared
|
|
103
|
+
* by root resolution and the init pointer guard so they can never
|
|
104
|
+
* disagree (slice 3.2).
|
|
105
|
+
*/
|
|
106
|
+
export declare function classifyOpenSpecDir(projectRoot: string): OpenSpecDirClassification;
|
|
64
107
|
//# sourceMappingURL=project-config.d.ts.map
|