@fission-ai/openspec 1.13.0 → 1.13.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 +1 -1
- package/dist/commands/change.js +12 -1
- package/dist/commands/config.d.ts +9 -0
- package/dist/commands/config.js +161 -22
- package/dist/commands/feedback.js +6 -4
- package/dist/commands/schema.js +30 -9
- package/dist/commands/store.js +5 -2
- package/dist/commands/validate.d.ts +7 -0
- package/dist/commands/validate.js +63 -1
- package/dist/commands/workflow/instructions.js +33 -14
- package/dist/commands/workflow/new-change.js +20 -0
- package/dist/commands/workflow/shared.js +8 -0
- package/dist/commands/workflow/status.d.ts +5 -1
- package/dist/commands/workflow/status.js +42 -5
- package/dist/core/archive.js +17 -1
- package/dist/core/artifact-graph/schema.d.ts +14 -0
- package/dist/core/artifact-graph/schema.js +52 -0
- package/dist/core/artifact-graph/types.js +12 -1
- package/dist/core/change-status-policy.d.ts +14 -0
- package/dist/core/change-status-policy.js +14 -6
- package/dist/core/command-generation/generator.js +2 -0
- package/dist/core/completion-tip.js +7 -7
- package/dist/core/completions/installers/bash-installer.js +15 -7
- package/dist/core/completions/installers/shell-quote.d.ts +11 -0
- package/dist/core/completions/installers/shell-quote.js +13 -0
- package/dist/core/completions/installers/zsh-installer.js +3 -2
- package/dist/core/config-schema.d.ts +1 -1
- package/dist/core/config.d.ts +16 -0
- package/dist/core/config.js +27 -1
- package/dist/core/global-config.d.ts +30 -2
- package/dist/core/global-config.js +57 -3
- package/dist/core/init.js +13 -3
- package/dist/core/legacy-cleanup.d.ts +4 -0
- package/dist/core/legacy-cleanup.js +180 -12
- package/dist/core/list.js +55 -11
- package/dist/core/migration.js +6 -1
- package/dist/core/parsers/change-parser.d.ts +12 -1
- package/dist/core/parsers/change-parser.js +85 -87
- package/dist/core/parsers/markdown-parser.js +4 -3
- package/dist/core/parsers/requirement-blocks.d.ts +44 -3
- package/dist/core/parsers/requirement-blocks.js +90 -15
- package/dist/core/parsers/requirement-text.d.ts +13 -2
- package/dist/core/parsers/requirement-text.js +34 -7
- package/dist/core/parsers/spec-structure.js +4 -1
- package/dist/core/project-config.d.ts +9 -1
- package/dist/core/project-config.js +11 -2
- package/dist/core/references.d.ts +20 -0
- package/dist/core/references.js +56 -0
- package/dist/core/shared/skill-content-equivalence.js +5 -1
- package/dist/core/shared/skill-generation.js +36 -8
- package/dist/core/shared/tool-detection.js +9 -3
- package/dist/core/specs-apply.js +82 -11
- package/dist/core/store/git.d.ts +33 -0
- package/dist/core/store/git.js +60 -6
- package/dist/core/store/operations.d.ts +1 -1
- package/dist/core/store/operations.js +32 -10
- package/dist/core/store/registry.d.ts +2 -1
- package/dist/core/store/registry.js +5 -1
- package/dist/core/templates/optional-workflow.d.ts +69 -0
- package/dist/core/templates/optional-workflow.js +140 -0
- package/dist/core/templates/workflows/apply-change.js +18 -3
- package/dist/core/templates/workflows/archive-change.js +50 -15
- package/dist/core/templates/workflows/bulk-archive-change.js +63 -13
- package/dist/core/templates/workflows/continue-change.js +14 -2
- package/dist/core/templates/workflows/explore.js +32 -14
- package/dist/core/templates/workflows/ff-change.js +19 -3
- package/dist/core/templates/workflows/new-change.js +15 -3
- package/dist/core/templates/workflows/onboard.js +67 -37
- package/dist/core/templates/workflows/project-root.d.ts +29 -0
- package/dist/core/templates/workflows/project-root.js +38 -0
- package/dist/core/templates/workflows/propose.js +22 -5
- package/dist/core/templates/workflows/sync-specs.js +40 -1
- package/dist/core/templates/workflows/update-change.js +52 -25
- package/dist/core/templates/workflows/verify-change.js +12 -3
- package/dist/core/update.d.ts +12 -0
- package/dist/core/update.js +58 -3
- package/dist/core/validation/task-checkboxes.d.ts +34 -0
- package/dist/core/validation/task-checkboxes.js +181 -0
- package/dist/core/validation/validator.d.ts +53 -1
- package/dist/core/validation/validator.js +154 -40
- package/dist/core/version-check.d.ts +14 -7
- package/dist/core/version-check.js +63 -17
- package/dist/core/worksets.js +6 -4
- package/dist/prompts/searchable-multi-select.d.ts +7 -0
- package/dist/prompts/searchable-multi-select.js +35 -6
- package/dist/telemetry/config.js +14 -2
- package/dist/telemetry/index.d.ts +4 -3
- package/dist/telemetry/index.js +22 -10
- package/dist/telemetry/opt-out.d.ts +17 -0
- package/dist/telemetry/opt-out.js +27 -0
- package/dist/utils/nested-change.d.ts +35 -0
- package/dist/utils/nested-change.js +186 -0
- package/dist/utils/spec-discovery.d.ts +20 -0
- package/dist/utils/spec-discovery.js +59 -0
- package/dist/utils/task-progress.d.ts +1 -1
- package/dist/utils/task-progress.js +38 -6
- package/package.json +4 -4
- package/schemas/spec-driven/schema.yaml +10 -3
- package/schemas/spec-driven/templates/design.md +2 -0
- package/schemas/spec-driven/templates/proposal.md +2 -0
- package/schemas/spec-driven/templates/spec.md +2 -0
- package/schemas/spec-driven/templates/tasks.md +2 -0
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { STORE_SELECTION_GUIDANCE } from './store-selection.js';
|
|
2
|
+
import { PROJECT_ROOT_GUARD } from './project-root.js';
|
|
2
3
|
export function getVerifyChangeSkillTemplate() {
|
|
3
4
|
return {
|
|
4
5
|
name: 'openspec-verify-change',
|
|
5
|
-
description: 'Verify implementation matches change artifacts. Use when the user wants to validate that implementation is complete, correct, and coherent before archiving.',
|
|
6
|
+
description: 'Verify implementation matches OpenSpec change artifacts. Use when the user wants to validate that implementation is complete, correct, and coherent before archiving. Also use when the user says "openspec verify" or "opsx verify".',
|
|
6
7
|
instructions: `Verify that an implementation matches the change artifacts (specs, tasks, design).
|
|
7
8
|
|
|
8
9
|
${STORE_SELECTION_GUIDANCE}
|
|
9
10
|
|
|
11
|
+
${PROJECT_ROOT_GUARD}
|
|
12
|
+
|
|
10
13
|
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
|
11
14
|
|
|
12
15
|
**Steps**
|
|
@@ -54,7 +57,9 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
54
57
|
|
|
55
58
|
**Task Completion**:
|
|
56
59
|
- If \`contextFiles.tasks\` exists, read every file path in it
|
|
57
|
-
- Parse checkboxes:
|
|
60
|
+
- Parse checkboxes: complete means the box holds only \`x\`/\`X\`, ignoring
|
|
61
|
+
spacing (\`- [ x]\` is complete); every other marker is incomplete
|
|
62
|
+
(\`- [ ]\`, \`- []\`, and unfamiliar ones such as \`- [~]\` or \`- [-]\`)
|
|
58
63
|
- Count complete vs total tasks
|
|
59
64
|
- If incomplete tasks exist:
|
|
60
65
|
- Add CRITICAL issue for each incomplete task
|
|
@@ -181,6 +186,8 @@ export function getOpsxVerifyCommandTemplate() {
|
|
|
181
186
|
|
|
182
187
|
${STORE_SELECTION_GUIDANCE}
|
|
183
188
|
|
|
189
|
+
${PROJECT_ROOT_GUARD}
|
|
190
|
+
|
|
184
191
|
**Input**: Optionally specify a change name after \`/opsx:verify\` (e.g., \`/opsx:verify add-auth\`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
|
185
192
|
|
|
186
193
|
**Steps**
|
|
@@ -228,7 +235,9 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
228
235
|
|
|
229
236
|
**Task Completion**:
|
|
230
237
|
- If \`contextFiles.tasks\` exists, read every file path in it
|
|
231
|
-
- Parse checkboxes:
|
|
238
|
+
- Parse checkboxes: complete means the box holds only \`x\`/\`X\`, ignoring
|
|
239
|
+
spacing (\`- [ x]\` is complete); every other marker is incomplete
|
|
240
|
+
(\`- [ ]\`, \`- []\`, and unfamiliar ones such as \`- [~]\` or \`- [-]\`)
|
|
232
241
|
- Count complete vs total tasks
|
|
233
242
|
- If incomplete tasks exist:
|
|
234
243
|
- Add CRITICAL issue for each incomplete task
|
package/dist/core/update.d.ts
CHANGED
|
@@ -29,6 +29,18 @@ export declare class UpdateCommand {
|
|
|
29
29
|
*/
|
|
30
30
|
execute(projectPath: string): Promise<void>;
|
|
31
31
|
private syncCopilotCloudFiles;
|
|
32
|
+
/**
|
|
33
|
+
* Tools whose SKILL.md bodies no longer match what this CLI generates.
|
|
34
|
+
*
|
|
35
|
+
* Skill freshness was decided solely by the `generatedBy:` line, so a body
|
|
36
|
+
* edited after generation — a "helpful" PR touching `.claude/skills/**`, a
|
|
37
|
+
* dotfile sync, another agent — left `update` reporting the install healthy.
|
|
38
|
+
* Command files never had that gap: `areCommandFilesUpToDate` content-
|
|
39
|
+
* compares them, and the same comparison belongs on the higher-authority
|
|
40
|
+
* surface. A missing skill file is left to the profile-sync check, which
|
|
41
|
+
* already knows what a partial install means.
|
|
42
|
+
*/
|
|
43
|
+
private findToolsWithDriftedSkills;
|
|
32
44
|
/**
|
|
33
45
|
* Display message when all tools are up to date.
|
|
34
46
|
*/
|
package/dist/core/update.js
CHANGED
|
@@ -13,7 +13,7 @@ import { FileSystemUtils } from '../utils/file-system.js';
|
|
|
13
13
|
import { getSkillReferenceTransformer, getTransformerForTool, transformToSkillReferences } from '../utils/command-references.js';
|
|
14
14
|
import { AI_TOOLS, OPENSPEC_DIR_NAME } from './config.js';
|
|
15
15
|
import { generateCommands, CommandAdapterRegistry, } from './command-generation/index.js';
|
|
16
|
-
import { getToolVersionStatus, getSkillTemplates, getCommandContents, generateSkillContent, getToolsWithSkillsDir, hasGlobalSkillTarget, resolveToolSkillsDir, toolSupportsSkills, formatIdeRestart, } from './shared/index.js';
|
|
16
|
+
import { getToolSkillStatus, getToolVersionStatus, getSkillTemplates, getCommandContents, generateSkillContent, getToolsWithSkillsDir, hasGlobalSkillTarget, resolveToolSkillsDir, toolSupportsSkills, formatIdeRestart, } from './shared/index.js';
|
|
17
17
|
import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatDeferredGlobalPromptSummary, formatCleanupSummary, formatDetectionSummary, getLegacyGlobalPromptMatches, getLegacyWorkflowIdsForTool, getToolsFromLegacyArtifacts, omitGlobalLegacyPromptFiles, omitToolLegacyArtifacts, pickGlobalLegacyPromptFiles, } from './legacy-cleanup.js';
|
|
18
18
|
import { isInteractive } from '../utils/interactive.js';
|
|
19
19
|
import { getGlobalConfig } from './global-config.js';
|
|
@@ -27,6 +27,14 @@ import { resolveSharedSkillWriters, writeSharedSkillTarget, } from './shared-ski
|
|
|
27
27
|
import { includesGitHubCopilot, writeCopilotCloudFiles, removeCopilotCloudFiles, isCopilotCloudEnabled, readCopilotCloudOptIn, findUnmanagedCloudFiles } from './github-copilot/cloud-agent.js';
|
|
28
28
|
const require = createRequire(import.meta.url);
|
|
29
29
|
const { version: OPENSPEC_VERSION } = require('../../package.json');
|
|
30
|
+
/**
|
|
31
|
+
* Checkout artifacts that are not real content drift: a UTF-8 BOM and the CRLF
|
|
32
|
+
* line endings a Windows clone with `core.autocrlf` reintroduces on every
|
|
33
|
+
* checkout of committed generated files.
|
|
34
|
+
*/
|
|
35
|
+
function normalizeGeneratedFile(content) {
|
|
36
|
+
return content.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
|
|
37
|
+
}
|
|
30
38
|
/**
|
|
31
39
|
* Scans installed workflow artifacts (skills and managed commands) across all configured tools.
|
|
32
40
|
* Returns the union of detected workflow IDs that match ALL_WORKFLOWS.
|
|
@@ -120,9 +128,11 @@ export class UpdateCommand {
|
|
|
120
128
|
})
|
|
121
129
|
.map((s) => s.toolId);
|
|
122
130
|
const toolsNeedingConfigSync = getToolsNeedingProfileSync(resolvedProjectPath, desiredWorkflows, delivery, configuredTools);
|
|
131
|
+
const toolsWithDriftedSkills = this.findToolsWithDriftedSkills(resolvedProjectPath, configuredTools, delivery, (toolId) => legacyWorkflowOverrides[toolId] ?? desiredWorkflows);
|
|
123
132
|
const toolsToUpdateSet = new Set([
|
|
124
133
|
...toolsNeedingVersionUpdate,
|
|
125
134
|
...toolsNeedingConfigSync,
|
|
135
|
+
...toolsWithDriftedSkills,
|
|
126
136
|
]);
|
|
127
137
|
const toolsUpToDate = toolStatuses.filter((s) => !toolsToUpdateSet.has(s.toolId));
|
|
128
138
|
if (!this.force && toolsToUpdateSet.size === 0 && newlyConfiguredTools.length === 0) {
|
|
@@ -146,7 +156,7 @@ export class UpdateCommand {
|
|
|
146
156
|
console.log('No additional refresh needed after legacy migration.');
|
|
147
157
|
}
|
|
148
158
|
else {
|
|
149
|
-
this.displayUpdatePlan([...toolsToUpdateSet], statusByTool, toolsUpToDate);
|
|
159
|
+
this.displayUpdatePlan([...toolsToUpdateSet], statusByTool, toolsUpToDate, new Set(toolsWithDriftedSkills));
|
|
150
160
|
}
|
|
151
161
|
console.log();
|
|
152
162
|
// 9. Determine what to generate based on delivery
|
|
@@ -375,6 +385,45 @@ export class UpdateCommand {
|
|
|
375
385
|
console.warn(`Warning: failed to sync Copilot cloud agent files: ${message}`);
|
|
376
386
|
}
|
|
377
387
|
}
|
|
388
|
+
/**
|
|
389
|
+
* Tools whose SKILL.md bodies no longer match what this CLI generates.
|
|
390
|
+
*
|
|
391
|
+
* Skill freshness was decided solely by the `generatedBy:` line, so a body
|
|
392
|
+
* edited after generation — a "helpful" PR touching `.claude/skills/**`, a
|
|
393
|
+
* dotfile sync, another agent — left `update` reporting the install healthy.
|
|
394
|
+
* Command files never had that gap: `areCommandFilesUpToDate` content-
|
|
395
|
+
* compares them, and the same comparison belongs on the higher-authority
|
|
396
|
+
* surface. A missing skill file is left to the profile-sync check, which
|
|
397
|
+
* already knows what a partial install means.
|
|
398
|
+
*/
|
|
399
|
+
findToolsWithDriftedSkills(projectPath, toolIds, delivery, workflowsForTool) {
|
|
400
|
+
return toolIds.filter((toolId) => {
|
|
401
|
+
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
402
|
+
if (!tool || !toolSupportsSkills(tool))
|
|
403
|
+
return false;
|
|
404
|
+
if (!shouldGenerateSkillsForTool(tool.value, delivery))
|
|
405
|
+
return false;
|
|
406
|
+
// A shared skills root is generated with its owner's transformer, so
|
|
407
|
+
// only the owner may compare it. getToolSkillStatus settles ownership.
|
|
408
|
+
if (!getToolSkillStatus(projectPath, tool.value).configured)
|
|
409
|
+
return false;
|
|
410
|
+
const skillsDir = resolveToolSkillsDir(projectPath, tool);
|
|
411
|
+
const transformer = getTransformerForTool(tool.value, delivery, resolveCommandSurfaceCapability(tool.value), resolveCommandInvocation(tool.value));
|
|
412
|
+
return getSkillTemplates(workflowsForTool(toolId)).some(({ template, dirName }) => {
|
|
413
|
+
const skillFile = path.join(skillsDir, dirName, 'SKILL.md');
|
|
414
|
+
if (!fs.existsSync(skillFile))
|
|
415
|
+
return false;
|
|
416
|
+
try {
|
|
417
|
+
const existing = fs.readFileSync(skillFile, 'utf-8');
|
|
418
|
+
const generated = generateSkillContent(template, OPENSPEC_VERSION, transformer);
|
|
419
|
+
return normalizeGeneratedFile(existing) !== normalizeGeneratedFile(generated);
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
}
|
|
378
427
|
/**
|
|
379
428
|
* Display message when all tools are up to date.
|
|
380
429
|
*/
|
|
@@ -388,13 +437,18 @@ export class UpdateCommand {
|
|
|
388
437
|
/**
|
|
389
438
|
* Display the update plan showing which tools need updating.
|
|
390
439
|
*/
|
|
391
|
-
displayUpdatePlan(toolsToUpdate, statusByTool, upToDate) {
|
|
440
|
+
displayUpdatePlan(toolsToUpdate, statusByTool, upToDate, driftedSkills = new Set()) {
|
|
392
441
|
const updates = toolsToUpdate.map((toolId) => {
|
|
393
442
|
const status = statusByTool.get(toolId);
|
|
394
443
|
if (status?.needsUpdate) {
|
|
395
444
|
const fromVersion = status.generatedByVersion ?? 'unknown';
|
|
396
445
|
return `${status.toolId} (${fromVersion} → ${OPENSPEC_VERSION})`;
|
|
397
446
|
}
|
|
447
|
+
// Say why: a user who edited a SKILL.md on purpose is owed the reason
|
|
448
|
+
// their edit is about to be overwritten.
|
|
449
|
+
if (driftedSkills.has(toolId)) {
|
|
450
|
+
return `${toolId} (skill files differ from the generated content)`;
|
|
451
|
+
}
|
|
398
452
|
return `${toolId} (config sync)`;
|
|
399
453
|
});
|
|
400
454
|
console.log(`Updating ${toolsToUpdate.length} tool(s): ${updates.join(', ')}`);
|
|
@@ -826,6 +880,7 @@ export class UpdateCommand {
|
|
|
826
880
|
return {
|
|
827
881
|
name: tool?.name || toolId,
|
|
828
882
|
value: toolId,
|
|
883
|
+
searchAliases: tool?.searchAliases,
|
|
829
884
|
configured: false,
|
|
830
885
|
preSelected: true, // Pre-select all detected legacy tools
|
|
831
886
|
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface TaskCheckboxDocument {
|
|
2
|
+
path: string;
|
|
3
|
+
content: string;
|
|
4
|
+
}
|
|
5
|
+
export interface TaskCheckboxIssue {
|
|
6
|
+
path: string;
|
|
7
|
+
line: number;
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Reports tracked task files that list work as plain bullets or numbered items
|
|
12
|
+
* instead of checkboxes (#354).
|
|
13
|
+
*
|
|
14
|
+
* Progress counts checkboxes and nothing else, so a tasks file written as a
|
|
15
|
+
* bare list is worse than an empty one: `openspec list` prints "No tasks",
|
|
16
|
+
* `openspec status` treats the change as having no work left, and
|
|
17
|
+
* `openspec archive` has no incomplete task to warn about. The file looks
|
|
18
|
+
* finished to the tool and unfinished to the reader.
|
|
19
|
+
*
|
|
20
|
+
* Reported only when the change's *whole* tracked set has zero checkboxes.
|
|
21
|
+
* One nested tasks file of prose beside a real checklist is not the failure
|
|
22
|
+
* this catches, and a change that is mid-authoring keeps its progress the
|
|
23
|
+
* moment a single checkbox exists.
|
|
24
|
+
*
|
|
25
|
+
* The scan for the offending line looks at rendered content only: fenced
|
|
26
|
+
* blocks, HTML comments, YAML front matter and top-level indented code are
|
|
27
|
+
* skipped. Every one of those
|
|
28
|
+
* exclusions can only *silence* a warning, never drop a real task — that is the
|
|
29
|
+
* opposite trade from the task parser, where fence awareness would hide work
|
|
30
|
+
* that `archive` must still refuse, and it is why the parser stays literal
|
|
31
|
+
* while this scan does not.
|
|
32
|
+
*/
|
|
33
|
+
export declare function findMissingTaskCheckboxIssues(documents: readonly TaskCheckboxDocument[]): TaskCheckboxIssue[];
|
|
34
|
+
//# sourceMappingURL=task-checkboxes.d.ts.map
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { parseTaskLines } from '../../utils/task-progress.js';
|
|
2
|
+
/**
|
|
3
|
+
* A list item that carries text but no checkbox: `- item`, `* item`, `+ item`,
|
|
4
|
+
* `1. item`, `1) item`. Checkbox lines match this too, so callers must rule the
|
|
5
|
+
* document set out on checkbox count first.
|
|
6
|
+
*
|
|
7
|
+
* Matches at any indent; the caller decides how deep is too deep. See
|
|
8
|
+
* `CODE_BLOCK_COLUMN`.
|
|
9
|
+
*
|
|
10
|
+
* A thematic break (`---`, `***`, `- - -`) is not a list item: the run has no
|
|
11
|
+
* text after it, and this pattern requires a non-space character. `* * *` is
|
|
12
|
+
* the one break spelled like a list of `*` items, and it is accepted as a list
|
|
13
|
+
* item rather than special-cased, because a file whose only list-shaped line is
|
|
14
|
+
* a horizontal rule still has zero tasks — the warning stays true, it just
|
|
15
|
+
* points at an odd line.
|
|
16
|
+
*
|
|
17
|
+
* An ordered marker runs at most nine digits, as CommonMark and the scenario
|
|
18
|
+
* bullet pattern in `specs-apply.ts` both require.
|
|
19
|
+
*/
|
|
20
|
+
const LIST_ITEM = /^\s*(?:[-*+]|\d{1,9}[.)])\s+\S/;
|
|
21
|
+
/**
|
|
22
|
+
* The indent at which a top-level line stops being content and becomes an
|
|
23
|
+
* indented code block, per CommonMark.
|
|
24
|
+
*
|
|
25
|
+
* The fence logic already treats four spaces as code; the list scan did not,
|
|
26
|
+
* so a sample of terminal output written as ` - example output` was
|
|
27
|
+
* reported as an uncheckboxed task list, and under `--strict` that false
|
|
28
|
+
* positive failed validation on a correct file.
|
|
29
|
+
*
|
|
30
|
+
* Genuine nested lists survive the cut, because this scan reports the *first*
|
|
31
|
+
* list item it finds and a nested item always sits under a shallower parent.
|
|
32
|
+
* That parent is what gets reported, exactly as before. A list-shaped line
|
|
33
|
+
* four columns deep with no shallower item above it is not nested under
|
|
34
|
+
* anything, which is precisely what makes it code.
|
|
35
|
+
*/
|
|
36
|
+
const CODE_BLOCK_COLUMN = 4;
|
|
37
|
+
/** Leading whitespace of a line in visual columns, a tab counting as four. */
|
|
38
|
+
function indentColumns(line) {
|
|
39
|
+
let column = 0;
|
|
40
|
+
for (const char of line) {
|
|
41
|
+
if (char === ' ')
|
|
42
|
+
column += 1;
|
|
43
|
+
else if (char === '\t')
|
|
44
|
+
column += CODE_BLOCK_COLUMN - (column % CODE_BLOCK_COLUMN);
|
|
45
|
+
else
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
return column;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A fenced block delimiter: a run of three or more backticks or tildes, plus
|
|
52
|
+
* whatever follows it on the line (an info string on an opener, nothing on a
|
|
53
|
+
* valid closer).
|
|
54
|
+
*
|
|
55
|
+
* Indented by at most three spaces, as CommonMark requires: at four, the line is
|
|
56
|
+
* an indented code block rather than a fence, and treating it as an opener would
|
|
57
|
+
* leave the scan inside a block that never began and hide every list below it.
|
|
58
|
+
*
|
|
59
|
+
* Deliberately unanchored at the end: `.` does not match `\r`, so `(.*)$` would
|
|
60
|
+
* fail on every line of a CRLF file and blind the scan to fences entirely.
|
|
61
|
+
*/
|
|
62
|
+
const FENCE = /^ {0,3}(`{3,}|~{3,})(.*)/;
|
|
63
|
+
/**
|
|
64
|
+
* The YAML front-matter delimiter: exactly three dashes. A longer run is a
|
|
65
|
+
* thematic break, so `----` must not open a block that swallows the list under
|
|
66
|
+
* it until the next `---`.
|
|
67
|
+
*/
|
|
68
|
+
const FRONT_MATTER = /^-{3}\s*$/;
|
|
69
|
+
const COMMENT_OPEN = '<!--';
|
|
70
|
+
const COMMENT_CLOSE = '-->';
|
|
71
|
+
/**
|
|
72
|
+
* Reports tracked task files that list work as plain bullets or numbered items
|
|
73
|
+
* instead of checkboxes (#354).
|
|
74
|
+
*
|
|
75
|
+
* Progress counts checkboxes and nothing else, so a tasks file written as a
|
|
76
|
+
* bare list is worse than an empty one: `openspec list` prints "No tasks",
|
|
77
|
+
* `openspec status` treats the change as having no work left, and
|
|
78
|
+
* `openspec archive` has no incomplete task to warn about. The file looks
|
|
79
|
+
* finished to the tool and unfinished to the reader.
|
|
80
|
+
*
|
|
81
|
+
* Reported only when the change's *whole* tracked set has zero checkboxes.
|
|
82
|
+
* One nested tasks file of prose beside a real checklist is not the failure
|
|
83
|
+
* this catches, and a change that is mid-authoring keeps its progress the
|
|
84
|
+
* moment a single checkbox exists.
|
|
85
|
+
*
|
|
86
|
+
* The scan for the offending line looks at rendered content only: fenced
|
|
87
|
+
* blocks, HTML comments, YAML front matter and top-level indented code are
|
|
88
|
+
* skipped. Every one of those
|
|
89
|
+
* exclusions can only *silence* a warning, never drop a real task — that is the
|
|
90
|
+
* opposite trade from the task parser, where fence awareness would hide work
|
|
91
|
+
* that `archive` must still refuse, and it is why the parser stays literal
|
|
92
|
+
* while this scan does not.
|
|
93
|
+
*/
|
|
94
|
+
export function findMissingTaskCheckboxIssues(documents) {
|
|
95
|
+
if (documents.length === 0)
|
|
96
|
+
return [];
|
|
97
|
+
if (documents.some((document) => parseTaskLines(document.content).length > 0))
|
|
98
|
+
return [];
|
|
99
|
+
const issues = [];
|
|
100
|
+
for (const document of documents) {
|
|
101
|
+
const line = findFirstListItemLine(document.content);
|
|
102
|
+
if (line === undefined)
|
|
103
|
+
continue;
|
|
104
|
+
issues.push({
|
|
105
|
+
path: document.path,
|
|
106
|
+
line,
|
|
107
|
+
message: 'This change counts as 0 tasks: no line in its tracked task files is a checkbox, ' +
|
|
108
|
+
'so "openspec list" and "openspec status" report no work and "openspec archive" ' +
|
|
109
|
+
'has nothing to flag as incomplete. Write each task as "- [ ] 1.1 Description".',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return issues;
|
|
113
|
+
}
|
|
114
|
+
/** 1-based line of the first list item in rendered content, if any. */
|
|
115
|
+
function findFirstListItemLine(content) {
|
|
116
|
+
const lines = content.split('\n');
|
|
117
|
+
let openFence;
|
|
118
|
+
let inComment = false;
|
|
119
|
+
let index = skipFrontMatter(lines);
|
|
120
|
+
for (; index < lines.length; index++) {
|
|
121
|
+
const line = lines[index];
|
|
122
|
+
if (inComment) {
|
|
123
|
+
if (line.includes(COMMENT_CLOSE))
|
|
124
|
+
inComment = false;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const fence = line.match(FENCE);
|
|
128
|
+
if (fence) {
|
|
129
|
+
const marker = fence[1][0];
|
|
130
|
+
const length = fence[1].length;
|
|
131
|
+
if (openFence === undefined) {
|
|
132
|
+
openFence = { marker, length };
|
|
133
|
+
}
|
|
134
|
+
else if (marker === openFence.marker &&
|
|
135
|
+
length >= openFence.length &&
|
|
136
|
+
fence[2].trim() === '') {
|
|
137
|
+
// CommonMark: a closer matches its opener's character, runs at least as
|
|
138
|
+
// long, and carries no info string. A shorter or annotated run inside a
|
|
139
|
+
// block is content, so a ```` ``` ```` sample nested in a ```` ```` ````
|
|
140
|
+
// block does not end the block early and expose its bullets.
|
|
141
|
+
openFence = undefined;
|
|
142
|
+
}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (openFence !== undefined)
|
|
146
|
+
continue;
|
|
147
|
+
// Only a comment that opens the line hides it. `- [ ] 1.1 do it <!-- note`
|
|
148
|
+
// is a task line first, and the template's own `## 1. <!-- Task Group -->`
|
|
149
|
+
// must not swallow the checklist that follows it.
|
|
150
|
+
if (line.trimStart().startsWith(COMMENT_OPEN)) {
|
|
151
|
+
if (!line.includes(COMMENT_CLOSE, line.indexOf(COMMENT_OPEN) + COMMENT_OPEN.length)) {
|
|
152
|
+
inComment = true;
|
|
153
|
+
}
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (LIST_ITEM.test(line) && indentColumns(line) < CODE_BLOCK_COLUMN)
|
|
157
|
+
return index + 1;
|
|
158
|
+
}
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Index of the first line after a YAML front-matter block, or 0 when the
|
|
163
|
+
* document does not open with one. A list under `tags:` is metadata about the
|
|
164
|
+
* file, never the work it tracks, and pointing a "write checkboxes" warning at
|
|
165
|
+
* it would name the wrong line.
|
|
166
|
+
*/
|
|
167
|
+
function skipFrontMatter(lines) {
|
|
168
|
+
// A UTF-8 BOM, prepended by Windows editors and PowerShell redirects, would
|
|
169
|
+
// otherwise hide the opener and expose a `tags:` list to the scan.
|
|
170
|
+
if (lines.length === 0 || !FRONT_MATTER.test(lines[0].replace(/^/, '').trimEnd())) {
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
for (let index = 1; index < lines.length; index++) {
|
|
174
|
+
if (FRONT_MATTER.test(lines[index].trimEnd()))
|
|
175
|
+
return index + 1;
|
|
176
|
+
}
|
|
177
|
+
// An unterminated opener is a thematic break, not front matter: rewinding to
|
|
178
|
+
// the top keeps every list in the document visible to the scan.
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=task-checkboxes.js.map
|
|
@@ -29,7 +29,53 @@ export declare class Validator {
|
|
|
29
29
|
mainSpecsDir?: string;
|
|
30
30
|
projectRoot?: string;
|
|
31
31
|
}): Promise<ValidationReport>;
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Lints the change's task files.
|
|
34
|
+
*
|
|
35
|
+
* Two checks with deliberately different reach. Checkbox formatting is read
|
|
36
|
+
* from whatever the change's own schema declares as its tracked task output,
|
|
37
|
+
* because every schema's progress, apply and archive behavior is computed by
|
|
38
|
+
* counting checkboxes in exactly those files. Numbering stays scoped to the
|
|
39
|
+
* built-in `spec-driven` schema, whose template is the one that numbers tasks
|
|
40
|
+
* in the first place.
|
|
41
|
+
*
|
|
42
|
+
* The checkbox check never falls back to a bare top-level `tasks.md`: a file
|
|
43
|
+
* no artifact declares is not a tracked task list, and warning about its
|
|
44
|
+
* formatting would be a guess about a file the tool does not read.
|
|
45
|
+
*/
|
|
46
|
+
private collectTaskFileIssues;
|
|
47
|
+
/**
|
|
48
|
+
* Reads task files into change-relative documents, counting the ones that
|
|
49
|
+
* exist but could not be read. A file that is simply absent is not counted:
|
|
50
|
+
* the resolver only returns files it found, so the remaining read failures
|
|
51
|
+
* are permissions and I/O, and a check that reasons over the whole set needs
|
|
52
|
+
* to know its evidence was incomplete.
|
|
53
|
+
*/
|
|
54
|
+
private readTaskDocuments;
|
|
55
|
+
/**
|
|
56
|
+
* Names a task file relative to its change, POSIX-separated.
|
|
57
|
+
*
|
|
58
|
+
* Both sides are canonicalized first. `resolveArtifactOutputs` hands back real
|
|
59
|
+
* paths, while `changeDir` carries whatever spelling the caller resolved, and
|
|
60
|
+
* the two can differ without being apart: on Windows a short 8.3 alias
|
|
61
|
+
* (`RUNNER~1`) against its expanded form turned `tasks.md` into a
|
|
62
|
+
* `../../../..`-prefixed absolute path in the report, and a symlinked project
|
|
63
|
+
* directory does the same elsewhere. Canonicalizing recovers the real
|
|
64
|
+
* relationship. The Windows CI job is the regression guard — the mismatch
|
|
65
|
+
* cannot be staged on POSIX, where the spawned CLI's `process.cwd()` is
|
|
66
|
+
* already physical.
|
|
67
|
+
*
|
|
68
|
+
* A path that still escapes would be a resolver bug rather than a spelling
|
|
69
|
+
* difference, but the report must never leak an absolute filesystem path, so
|
|
70
|
+
* the file name stands in.
|
|
71
|
+
*/
|
|
72
|
+
private taskDocumentPath;
|
|
73
|
+
/**
|
|
74
|
+
* True when the change resolves to the package's own `spec-driven` schema. A
|
|
75
|
+
* project schema that merely reuses the name is not it, so checks written
|
|
76
|
+
* against the built-in template never fire on someone else's task format.
|
|
77
|
+
*/
|
|
78
|
+
private usesBuiltInSpecDrivenSchema;
|
|
33
79
|
/**
|
|
34
80
|
* Report MODIFIED requirements whose block omits a scenario the main spec
|
|
35
81
|
* still carries. Uses the same comparison archive applies, so validate can
|
|
@@ -72,6 +118,12 @@ export declare class Validator {
|
|
|
72
118
|
*/
|
|
73
119
|
private buildMissingShallOrMustMessage;
|
|
74
120
|
private countScenarios;
|
|
121
|
+
/**
|
|
122
|
+
* Why a requirement with a visible scenario header still has no scenario:
|
|
123
|
+
* the header has no body, and the spec path archive validates against does
|
|
124
|
+
* not count it. Empty when the block has no bare scenario header.
|
|
125
|
+
*/
|
|
126
|
+
private emptyScenarioHint;
|
|
75
127
|
private formatSectionList;
|
|
76
128
|
}
|
|
77
129
|
//# sourceMappingURL=validator.d.ts.map
|