@codewalla_india/openspec 1.3.1 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -22
- package/README.md +214 -133
- package/bin/openspec.js +5 -5
- package/dist/cli/index.js +99 -62
- package/dist/commands/change.d.ts +0 -2
- package/dist/commands/change.js +107 -86
- package/dist/commands/config.js +14 -16
- package/dist/commands/doctor.js +9 -4
- package/dist/commands/feedback.js +79 -30
- package/dist/commands/modify.d.ts +26 -0
- package/dist/commands/modify.js +147 -0
- package/dist/commands/schema.js +170 -91
- package/dist/commands/show.js +5 -2
- package/dist/commands/spec.js +49 -29
- package/dist/commands/store.js +15 -21
- package/dist/commands/validate.d.ts +8 -0
- package/dist/commands/validate.js +43 -20
- package/dist/commands/workflow/index.d.ts +2 -2
- package/dist/commands/workflow/index.js +1 -1
- package/dist/commands/workflow/instructions.d.ts +6 -26
- package/dist/commands/workflow/instructions.js +183 -376
- package/dist/commands/workflow/new-change.d.ts +0 -4
- package/dist/commands/workflow/new-change.js +10 -28
- package/dist/commands/workflow/shared.d.ts +34 -19
- package/dist/commands/workflow/shared.js +34 -4
- package/dist/commands/workflow/status.js +10 -32
- package/dist/commands/workflow/templates.js +15 -5
- package/dist/commands/workset.d.ts +2 -2
- package/dist/commands/workset.js +19 -22
- package/dist/comprehension-quiz/fingerprint.d.ts +39 -0
- package/dist/comprehension-quiz/fingerprint.js +84 -0
- package/dist/comprehension-quiz/index.d.ts +42 -0
- package/dist/comprehension-quiz/index.js +47 -0
- package/dist/comprehension-quiz/pass-record.d.ts +44 -0
- package/dist/comprehension-quiz/pass-record.js +98 -0
- package/dist/comprehension-quiz/providers/anthropic.d.ts +35 -0
- package/dist/comprehension-quiz/providers/anthropic.js +69 -0
- package/dist/comprehension-quiz/providers/factory.d.ts +44 -0
- package/dist/comprehension-quiz/providers/factory.js +174 -0
- package/dist/comprehension-quiz/providers/interface.d.ts +98 -0
- package/dist/comprehension-quiz/providers/interface.js +150 -0
- package/dist/comprehension-quiz/providers/local.d.ts +35 -0
- package/dist/comprehension-quiz/providers/local.js +69 -0
- package/dist/comprehension-quiz/providers/ollama.d.ts +35 -0
- package/dist/comprehension-quiz/providers/ollama.js +69 -0
- package/dist/comprehension-quiz/providers/openai.d.ts +35 -0
- package/dist/comprehension-quiz/providers/openai.js +72 -0
- package/dist/comprehension-quiz/question-generator.d.ts +117 -0
- package/dist/comprehension-quiz/question-generator.js +203 -0
- package/dist/comprehension-quiz/quiz-executor.d.ts +75 -0
- package/dist/comprehension-quiz/quiz-executor.js +181 -0
- package/dist/comprehension-quiz/types.d.ts +137 -0
- package/dist/comprehension-quiz/types.js +11 -0
- package/dist/core/archive.d.ts +14 -1
- package/dist/core/archive.js +1350 -190
- package/dist/core/artifact-graph/graph.d.ts +28 -9
- package/dist/core/artifact-graph/graph.js +94 -43
- package/dist/core/artifact-graph/index.d.ts +1 -1
- package/dist/core/artifact-graph/index.js +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +30 -3
- package/dist/core/artifact-graph/instruction-loader.js +70 -12
- package/dist/core/artifact-graph/outputs.d.ts +1 -0
- package/dist/core/artifact-graph/outputs.js +67 -5
- package/dist/core/artifact-graph/resolver.d.ts +14 -0
- package/dist/core/artifact-graph/resolver.js +69 -15
- package/dist/core/artifact-graph/types.js +21 -3
- package/dist/core/available-tools.d.ts +2 -2
- package/dist/core/available-tools.js +14 -3
- package/dist/core/change-metadata/schema.d.ts +2 -0
- package/dist/core/change-metadata/schema.js +13 -0
- package/dist/core/change-status-policy.d.ts +10 -1
- package/dist/core/change-status-policy.js +30 -2
- package/dist/core/command-generation/adapters/amazon-q.d.ts +4 -0
- package/dist/core/command-generation/adapters/amazon-q.js +11 -5
- package/dist/core/command-generation/adapters/antigravity.js +6 -5
- package/dist/core/command-generation/adapters/auggie.js +7 -6
- package/dist/core/command-generation/adapters/bob.d.ts +5 -1
- package/dist/core/command-generation/adapters/bob.js +11 -10
- package/dist/core/command-generation/adapters/claude.d.ts +1 -1
- package/dist/core/command-generation/adapters/claude.js +12 -17
- package/dist/core/command-generation/adapters/cline.js +5 -5
- package/dist/core/command-generation/adapters/codebuddy.js +8 -7
- package/dist/core/command-generation/adapters/continue.js +8 -7
- package/dist/core/command-generation/adapters/costrict.js +7 -6
- package/dist/core/command-generation/adapters/crush.js +9 -9
- package/dist/core/command-generation/adapters/cursor.js +8 -8
- package/dist/core/command-generation/adapters/devin.d.ts +19 -0
- package/dist/core/command-generation/adapters/devin.js +36 -0
- package/dist/core/command-generation/adapters/factory.js +7 -6
- package/dist/core/command-generation/adapters/gemini.js +40 -5
- package/dist/core/command-generation/adapters/github-copilot.js +6 -5
- package/dist/core/command-generation/adapters/iflow.js +9 -8
- package/dist/core/command-generation/adapters/index.d.ts +4 -2
- package/dist/core/command-generation/adapters/index.js +4 -2
- package/dist/core/command-generation/adapters/junie.js +6 -5
- package/dist/core/command-generation/adapters/kilocode.js +1 -1
- package/dist/core/command-generation/adapters/kiro.js +6 -5
- package/dist/core/command-generation/adapters/lingma.js +9 -9
- package/dist/core/command-generation/adapters/oh-my-pi.d.ts +21 -0
- package/dist/core/command-generation/adapters/oh-my-pi.js +42 -0
- package/dist/core/command-generation/adapters/opencode.js +6 -8
- package/dist/core/command-generation/adapters/pi.d.ts +2 -2
- package/dist/core/command-generation/adapters/pi.js +7 -10
- package/dist/core/command-generation/adapters/qoder.js +9 -9
- package/dist/core/command-generation/adapters/qwen.d.ts +7 -3
- package/dist/core/command-generation/adapters/qwen.js +14 -9
- package/dist/core/command-generation/adapters/roocode.d.ts +4 -4
- package/dist/core/command-generation/adapters/roocode.js +9 -9
- package/dist/core/command-generation/adapters/trae.d.ts +13 -0
- package/dist/core/command-generation/adapters/trae.js +28 -0
- package/dist/core/command-generation/adapters/zcode.d.ts +16 -0
- package/dist/core/command-generation/adapters/zcode.js +33 -0
- package/dist/core/command-generation/generator.d.ts +8 -0
- package/dist/core/command-generation/generator.js +15 -1
- package/dist/core/command-generation/index.d.ts +1 -1
- package/dist/core/command-generation/index.js +1 -1
- package/dist/core/command-generation/invocation.d.ts +73 -0
- package/dist/core/command-generation/invocation.js +77 -0
- package/dist/core/command-generation/registry.js +8 -4
- package/dist/core/command-generation/types.d.ts +9 -2
- package/dist/core/command-generation/yaml.d.ts +20 -6
- package/dist/core/command-generation/yaml.js +31 -21
- package/dist/core/command-surface.d.ts +15 -0
- package/dist/core/command-surface.js +33 -0
- package/dist/core/completions/command-registry.js +16 -61
- package/dist/core/completions/generators/bash-generator.js +41 -41
- package/dist/core/completions/generators/fish-generator.js +7 -7
- package/dist/core/completions/generators/powershell-generator.js +44 -37
- package/dist/core/completions/generators/zsh-generator.js +33 -33
- package/dist/core/completions/installers/zsh-installer.d.ts +9 -0
- package/dist/core/completions/installers/zsh-installer.js +20 -4
- package/dist/core/completions/templates/bash-templates.js +24 -24
- package/dist/core/completions/templates/fish-templates.js +38 -38
- package/dist/core/completions/templates/powershell-templates.js +28 -28
- package/dist/core/completions/templates/zsh-templates.js +39 -39
- package/dist/core/config-prompts.d.ts +1 -1
- package/dist/core/config-prompts.js +14 -1
- package/dist/core/config-schema.d.ts +10 -1
- package/dist/core/config-schema.js +67 -1
- package/dist/core/config.d.ts +15 -0
- package/dist/core/config.js +46 -5
- package/dist/core/file-state.js +56 -24
- package/dist/core/github-copilot/cloud-agent.d.ts +95 -0
- package/dist/core/github-copilot/cloud-agent.js +497 -0
- package/dist/core/global-config.d.ts +16 -0
- package/dist/core/init.d.ts +58 -0
- package/dist/core/init.js +474 -73
- package/dist/core/legacy-cleanup.d.ts +59 -0
- package/dist/core/legacy-cleanup.js +253 -24
- package/dist/core/list.js +23 -16
- package/dist/core/migration.d.ts +73 -1
- package/dist/core/migration.js +360 -11
- package/dist/core/modify/conflict-detector.d.ts +26 -0
- package/dist/core/modify/conflict-detector.js +62 -0
- package/dist/core/modify/dependency-propagator.d.ts +38 -0
- package/dist/core/modify/dependency-propagator.js +103 -0
- package/dist/core/modify/history-tracker.d.ts +34 -0
- package/dist/core/modify/history-tracker.js +114 -0
- package/dist/core/modify/index.d.ts +7 -0
- package/dist/core/modify/index.js +7 -0
- package/dist/core/onboarding-commands.d.ts +30 -0
- package/dist/core/onboarding-commands.js +38 -0
- package/dist/core/openspec-root.js +18 -15
- package/dist/core/parsers/change-parser.d.ts +17 -2
- package/dist/core/parsers/change-parser.js +35 -21
- package/dist/core/parsers/code-fence.d.ts +15 -0
- package/dist/core/parsers/code-fence.js +49 -0
- package/dist/core/parsers/markdown-parser.d.ts +0 -3
- package/dist/core/parsers/markdown-parser.js +7 -60
- package/dist/core/parsers/requirement-blocks.d.ts +29 -0
- package/dist/core/parsers/requirement-blocks.js +150 -31
- package/dist/core/parsers/requirement-text.d.ts +48 -0
- package/dist/core/parsers/requirement-text.js +98 -0
- package/dist/core/parsers/spec-structure.d.ts +1 -1
- package/dist/core/parsers/spec-structure.js +20 -30
- package/dist/core/profile-sync-drift.d.ts +0 -8
- package/dist/core/profile-sync-drift.js +49 -50
- package/dist/core/profiles.d.ts +2 -2
- package/dist/core/profiles.js +2 -2
- package/dist/core/project-config.d.ts +27 -18
- package/dist/core/project-config.js +101 -34
- package/dist/core/references.js +42 -4
- package/dist/core/relationship-health.d.ts +8 -0
- package/dist/core/relationship-health.js +13 -0
- package/dist/core/root-selection.d.ts +9 -6
- package/dist/core/root-selection.js +42 -5
- 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/index.d.ts +1 -0
- package/dist/core/shared/index.js +1 -0
- package/dist/core/shared/skill-content-equivalence.d.ts +7 -0
- package/dist/core/shared/skill-content-equivalence.js +41 -0
- package/dist/core/shared/skill-generation.js +17 -13
- package/dist/core/shared/skill-paths.d.ts +13 -0
- package/dist/core/shared/skill-paths.js +23 -0
- package/dist/core/shared/tool-detection.d.ts +27 -7
- package/dist/core/shared/tool-detection.js +167 -37
- package/dist/core/shared-skill-target.d.ts +16 -0
- package/dist/core/shared-skill-target.js +156 -0
- package/dist/core/specs-apply.d.ts +80 -33
- package/dist/core/specs-apply.js +667 -145
- package/dist/core/store/git.d.ts +13 -0
- package/dist/core/store/git.js +24 -0
- package/dist/core/store/operations.js +20 -0
- package/dist/core/templates/skill-templates.d.ts +3 -2
- package/dist/core/templates/skill-templates.js +3 -2
- package/dist/core/templates/workflows/apply-change.d.ts +10 -0
- package/dist/core/templates/workflows/apply-change.js +186 -224
- package/dist/core/templates/workflows/archive-change.js +387 -260
- package/dist/core/templates/workflows/bulk-archive-change.js +655 -475
- package/dist/core/templates/workflows/continue-change.js +210 -219
- package/dist/core/templates/workflows/explore.js +479 -443
- package/dist/core/templates/workflows/feedback.js +97 -97
- package/dist/core/templates/workflows/ff-change.js +200 -191
- package/dist/core/templates/workflows/modify-change.d.ts +4 -1
- package/dist/core/templates/workflows/modify-change.js +193 -71
- package/dist/core/templates/workflows/new-change.js +127 -134
- package/dist/core/templates/workflows/onboard.js +548 -542
- package/dist/core/templates/workflows/propose.js +273 -217
- package/dist/core/templates/workflows/store-selection.d.ts +1 -1
- package/dist/core/templates/workflows/store-selection.js +1 -1
- package/dist/core/templates/workflows/sync-specs.js +500 -273
- package/dist/core/templates/workflows/update-change.d.ts +10 -0
- package/dist/core/templates/workflows/update-change.js +177 -0
- package/dist/core/templates/workflows/verify-change.js +324 -326
- package/dist/core/update.d.ts +40 -5
- package/dist/core/update.js +411 -96
- package/dist/core/validation/constants.d.ts +4 -1
- package/dist/core/validation/constants.js +4 -1
- package/dist/core/validation/plan-validator.d.ts +8 -0
- package/dist/core/validation/plan-validator.js +45 -0
- package/dist/core/validation/task-numbering.d.ts +16 -0
- package/dist/core/validation/task-numbering.js +57 -0
- package/dist/core/validation/validator.d.ts +27 -3
- package/dist/core/validation/validator.js +337 -48
- package/dist/core/version-check.d.ts +152 -0
- package/dist/core/version-check.js +731 -0
- package/dist/core/view.js +13 -18
- package/dist/prompts/searchable-multi-select.js +1 -1
- package/dist/telemetry/caller-detection.d.ts +27 -0
- package/dist/telemetry/caller-detection.js +85 -0
- package/dist/telemetry/caller-detection.test.d.ts +5 -0
- package/dist/telemetry/config.d.ts +48 -2
- package/dist/telemetry/config.js +113 -11
- package/dist/telemetry/config.test.d.ts +5 -0
- package/dist/telemetry/deduplication.d.ts +35 -0
- package/dist/telemetry/deduplication.js +82 -0
- package/dist/telemetry/deduplication.test.d.ts +5 -0
- package/dist/telemetry/identity.d.ts +62 -22
- package/dist/telemetry/identity.js +147 -102
- package/dist/telemetry/index.d.ts +126 -15
- package/dist/telemetry/index.js +569 -37
- package/dist/telemetry/sanitization.d.ts +40 -0
- package/dist/telemetry/sanitization.js +114 -0
- package/dist/telemetry/sanitization.test.d.ts +5 -0
- package/dist/telemetry/throttling.d.ts +20 -0
- package/dist/telemetry/throttling.js +70 -0
- package/dist/ui/welcome-screen.d.ts +9 -1
- package/dist/ui/welcome-screen.js +91 -38
- package/dist/utils/change-metadata.d.ts +43 -0
- package/dist/utils/change-metadata.js +123 -7
- package/dist/utils/change-utils.d.ts +10 -4
- package/dist/utils/change-utils.js +20 -13
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +16 -0
- package/dist/utils/command-references.d.ts +92 -6
- package/dist/utils/command-references.js +180 -7
- package/dist/utils/date.d.ts +8 -0
- package/dist/utils/date.js +13 -0
- package/dist/utils/file-system.d.ts +10 -0
- package/dist/utils/file-system.js +70 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/interactive.d.ts +23 -0
- package/dist/utils/interactive.js +32 -0
- package/dist/utils/item-discovery.d.ts +19 -0
- package/dist/utils/item-discovery.js +30 -48
- package/dist/utils/shell-detection.d.ts +1 -1
- package/dist/utils/shell-detection.js +64 -11
- package/dist/utils/spec-discovery.d.ts +40 -0
- package/dist/utils/spec-discovery.js +115 -0
- package/dist/utils/task-progress.d.ts +29 -1
- package/dist/utils/task-progress.js +109 -15
- package/package.json +96 -84
- package/schemas/spec-driven/schema.yaml +252 -184
- package/schemas/spec-driven/templates/design.md +19 -19
- package/schemas/spec-driven/templates/plan-template.md +91 -0
- package/schemas/spec-driven/templates/proposal.md +29 -24
- package/schemas/spec-driven/templates/spec.md +11 -8
- package/schemas/spec-driven/templates/tasks.md +9 -9
- package/scripts/postinstall.js +83 -83
- package/dist/core/command-generation/adapters/codex.d.ts +0 -16
- package/dist/core/command-generation/adapters/codex.js +0 -39
- package/dist/core/command-generation/adapters/windsurf.d.ts +0 -14
- package/dist/core/command-generation/adapters/windsurf.js +0 -38
- package/dist/core/comprehension/config.d.ts +0 -20
- package/dist/core/comprehension/config.js +0 -23
- package/dist/core/comprehension/fingerprint.d.ts +0 -14
- package/dist/core/comprehension/fingerprint.js +0 -33
- package/dist/core/comprehension/index.d.ts +0 -57
- package/dist/core/comprehension/index.js +0 -87
- package/dist/core/comprehension/pass-record.d.ts +0 -29
- package/dist/core/comprehension/pass-record.js +0 -64
- package/dist/core/comprehension/stats.d.ts +0 -35
- package/dist/core/comprehension/stats.js +0 -133
- package/dist/core/templates/workflows/comprehension-guidance.d.ts +0 -9
- package/dist/core/templates/workflows/comprehension-guidance.js +0 -73
- package/dist/core/templates/workflows/mcp-guidance.d.ts +0 -13
- package/dist/core/templates/workflows/mcp-guidance.js +0 -131
- package/dist/core/templates/workflows/user-prompt-guidance.d.ts +0 -16
- package/dist/core/templates/workflows/user-prompt-guidance.js +0 -48
- package/dist/telemetry/caller.d.ts +0 -5
- package/dist/telemetry/caller.js +0 -29
- package/dist/telemetry/client.d.ts +0 -27
- package/dist/telemetry/client.js +0 -127
- package/dist/telemetry/command-context.d.ts +0 -13
- package/dist/telemetry/command-context.js +0 -59
- package/dist/telemetry/comprehension.d.ts +0 -44
- package/dist/telemetry/comprehension.js +0 -105
- package/dist/telemetry/content.d.ts +0 -10
- package/dist/telemetry/content.js +0 -56
- package/dist/telemetry/git-stats.d.ts +0 -12
- package/dist/telemetry/git-stats.js +0 -69
- package/dist/telemetry/identify-cache.d.ts +0 -7
- package/dist/telemetry/identify-cache.js +0 -47
- package/dist/telemetry/input.d.ts +0 -17
- package/dist/telemetry/input.js +0 -68
- package/dist/telemetry/marker.d.ts +0 -37
- package/dist/telemetry/marker.js +0 -67
- package/dist/telemetry/workflow.d.ts +0 -85
- package/dist/telemetry/workflow.js +0 -318
- package/schemas/spec-driven/templates/plan.md +0 -19
|
@@ -6,7 +6,23 @@ import type { Artifact, SchemaYaml, CompletedSet, BlockedArtifacts } from './typ
|
|
|
6
6
|
export declare class ArtifactGraph {
|
|
7
7
|
private artifacts;
|
|
8
8
|
private schema;
|
|
9
|
+
/** Artifact id -> its position in the schema's `artifacts:` list. */
|
|
10
|
+
private declarationOrder;
|
|
9
11
|
private constructor();
|
|
12
|
+
/**
|
|
13
|
+
* Orders artifact ids by where the schema declares them.
|
|
14
|
+
*
|
|
15
|
+
* The dependency graph leaves siblings tied - spec-driven's `specs` and
|
|
16
|
+
* `design` both require only `proposal`, so both become ready at the same
|
|
17
|
+
* time. Ties used to be broken alphabetically, which put `design` ahead of
|
|
18
|
+
* `specs` and made the CLI recommend the artifacts in an order that
|
|
19
|
+
* contradicted the schema's own documented sequence
|
|
20
|
+
* (proposal -> specs -> design -> tasks). Breaking ties by declaration order
|
|
21
|
+
* follows the sequence the schema author wrote, for built-in and custom
|
|
22
|
+
* schemas alike, and stays just as deterministic. Ids not in the schema sort
|
|
23
|
+
* last so the comparator stays total.
|
|
24
|
+
*/
|
|
25
|
+
private compareByDeclarationOrder;
|
|
10
26
|
/**
|
|
11
27
|
* Creates an ArtifactGraph from a YAML file path.
|
|
12
28
|
*/
|
|
@@ -35,15 +51,6 @@ export declare class ArtifactGraph {
|
|
|
35
51
|
* Gets the schema version.
|
|
36
52
|
*/
|
|
37
53
|
getVersion(): number;
|
|
38
|
-
/**
|
|
39
|
-
* Builds reverse adjacency: artifact ID -> IDs of artifacts that depend on it.
|
|
40
|
-
*/
|
|
41
|
-
private buildDependentsMap;
|
|
42
|
-
/**
|
|
43
|
-
* Returns all transitive downstream artifact IDs that depend on the given artifact,
|
|
44
|
-
* sorted by build order (excludes the source artifact itself).
|
|
45
|
-
*/
|
|
46
|
-
getTransitiveDependents(artifactId: string): string[];
|
|
47
54
|
/**
|
|
48
55
|
* Computes the topological build order using Kahn's algorithm.
|
|
49
56
|
* Returns artifact IDs in the order they should be built.
|
|
@@ -61,5 +68,17 @@ export declare class ArtifactGraph {
|
|
|
61
68
|
* Gets blocked artifacts and their unmet dependencies.
|
|
62
69
|
*/
|
|
63
70
|
getBlocked(completed: CompletedSet): BlockedArtifacts;
|
|
71
|
+
/**
|
|
72
|
+
* Gets all artifacts that depend on the given artifact (direct and transitive).
|
|
73
|
+
* Returns artifact IDs in reverse topological order (deepest dependents first).
|
|
74
|
+
* Throws an error if a circular dependency is detected.
|
|
75
|
+
*/
|
|
76
|
+
getDependents(artifactId: string): string[];
|
|
77
|
+
/**
|
|
78
|
+
* Gets reverse topological order for updating artifacts.
|
|
79
|
+
* Returns artifact IDs in the order they should be updated (dependents before dependees).
|
|
80
|
+
* Throws an error if a circular dependency is detected.
|
|
81
|
+
*/
|
|
82
|
+
getReverseUpdateOrder(): string[];
|
|
64
83
|
}
|
|
65
84
|
//# sourceMappingURL=graph.d.ts.map
|
|
@@ -6,9 +6,29 @@ import { loadSchema, parseSchema } from './schema.js';
|
|
|
6
6
|
export class ArtifactGraph {
|
|
7
7
|
artifacts;
|
|
8
8
|
schema;
|
|
9
|
+
/** Artifact id -> its position in the schema's `artifacts:` list. */
|
|
10
|
+
declarationOrder;
|
|
9
11
|
constructor(schema) {
|
|
10
12
|
this.schema = schema;
|
|
11
13
|
this.artifacts = new Map(schema.artifacts.map(a => [a.id, a]));
|
|
14
|
+
this.declarationOrder = new Map(schema.artifacts.map((a, index) => [a.id, index]));
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Orders artifact ids by where the schema declares them.
|
|
18
|
+
*
|
|
19
|
+
* The dependency graph leaves siblings tied - spec-driven's `specs` and
|
|
20
|
+
* `design` both require only `proposal`, so both become ready at the same
|
|
21
|
+
* time. Ties used to be broken alphabetically, which put `design` ahead of
|
|
22
|
+
* `specs` and made the CLI recommend the artifacts in an order that
|
|
23
|
+
* contradicted the schema's own documented sequence
|
|
24
|
+
* (proposal -> specs -> design -> tasks). Breaking ties by declaration order
|
|
25
|
+
* follows the sequence the schema author wrote, for built-in and custom
|
|
26
|
+
* schemas alike, and stays just as deterministic. Ids not in the schema sort
|
|
27
|
+
* last so the comparator stays total.
|
|
28
|
+
*/
|
|
29
|
+
compareByDeclarationOrder(a, b) {
|
|
30
|
+
return ((this.declarationOrder.get(a) ?? Number.MAX_SAFE_INTEGER) -
|
|
31
|
+
(this.declarationOrder.get(b) ?? Number.MAX_SAFE_INTEGER));
|
|
12
32
|
}
|
|
13
33
|
/**
|
|
14
34
|
* Creates an ArtifactGraph from a YAML file path.
|
|
@@ -55,57 +75,27 @@ export class ArtifactGraph {
|
|
|
55
75
|
return this.schema.version;
|
|
56
76
|
}
|
|
57
77
|
/**
|
|
58
|
-
*
|
|
78
|
+
* Computes the topological build order using Kahn's algorithm.
|
|
79
|
+
* Returns artifact IDs in the order they should be built.
|
|
59
80
|
*/
|
|
60
|
-
|
|
81
|
+
getBuildOrder() {
|
|
82
|
+
const inDegree = new Map();
|
|
61
83
|
const dependents = new Map();
|
|
84
|
+
// Initialize all artifacts
|
|
62
85
|
for (const artifact of this.artifacts.values()) {
|
|
86
|
+
inDegree.set(artifact.id, artifact.requires.length);
|
|
63
87
|
dependents.set(artifact.id, []);
|
|
64
88
|
}
|
|
89
|
+
// Build reverse adjacency (who depends on whom)
|
|
65
90
|
for (const artifact of this.artifacts.values()) {
|
|
66
91
|
for (const req of artifact.requires) {
|
|
67
92
|
dependents.get(req).push(artifact.id);
|
|
68
93
|
}
|
|
69
94
|
}
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Returns all transitive downstream artifact IDs that depend on the given artifact,
|
|
74
|
-
* sorted by build order (excludes the source artifact itself).
|
|
75
|
-
*/
|
|
76
|
-
getTransitiveDependents(artifactId) {
|
|
77
|
-
if (!this.artifacts.has(artifactId)) {
|
|
78
|
-
return [];
|
|
79
|
-
}
|
|
80
|
-
const dependents = this.buildDependentsMap();
|
|
81
|
-
const collected = new Set();
|
|
82
|
-
const queue = [artifactId];
|
|
83
|
-
while (queue.length > 0) {
|
|
84
|
-
const current = queue.shift();
|
|
85
|
-
for (const dependentId of dependents.get(current) ?? []) {
|
|
86
|
-
if (!collected.has(dependentId)) {
|
|
87
|
-
collected.add(dependentId);
|
|
88
|
-
queue.push(dependentId);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return this.getBuildOrder().filter((id) => collected.has(id));
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Computes the topological build order using Kahn's algorithm.
|
|
96
|
-
* Returns artifact IDs in the order they should be built.
|
|
97
|
-
*/
|
|
98
|
-
getBuildOrder() {
|
|
99
|
-
const inDegree = new Map();
|
|
100
|
-
const dependents = this.buildDependentsMap();
|
|
101
|
-
// Initialize in-degrees
|
|
102
|
-
for (const artifact of this.artifacts.values()) {
|
|
103
|
-
inDegree.set(artifact.id, artifact.requires.length);
|
|
104
|
-
}
|
|
105
|
-
// Start with roots (in-degree 0), sorted for determinism
|
|
95
|
+
// Start with roots (in-degree 0), in declaration order for determinism
|
|
106
96
|
const queue = [...this.artifacts.keys()]
|
|
107
97
|
.filter(id => inDegree.get(id) === 0)
|
|
108
|
-
.sort();
|
|
98
|
+
.sort((a, b) => this.compareByDeclarationOrder(a, b));
|
|
109
99
|
const result = [];
|
|
110
100
|
while (queue.length > 0) {
|
|
111
101
|
const current = queue.shift();
|
|
@@ -119,7 +109,10 @@ export class ArtifactGraph {
|
|
|
119
109
|
newlyReady.push(dep);
|
|
120
110
|
}
|
|
121
111
|
}
|
|
122
|
-
queue
|
|
112
|
+
// Re-sort the whole queue, not just the new arrivals: an artifact that
|
|
113
|
+
// has been waiting can be declared after one that just became ready.
|
|
114
|
+
queue.push(...newlyReady);
|
|
115
|
+
queue.sort((a, b) => this.compareByDeclarationOrder(a, b));
|
|
123
116
|
}
|
|
124
117
|
return result;
|
|
125
118
|
}
|
|
@@ -137,8 +130,9 @@ export class ArtifactGraph {
|
|
|
137
130
|
ready.push(artifact.id);
|
|
138
131
|
}
|
|
139
132
|
}
|
|
140
|
-
//
|
|
141
|
-
|
|
133
|
+
// Declaration order: deterministic, and the first entry is the artifact the
|
|
134
|
+
// schema wants written next.
|
|
135
|
+
return ready.sort((a, b) => this.compareByDeclarationOrder(a, b));
|
|
142
136
|
}
|
|
143
137
|
/**
|
|
144
138
|
* Checks if all artifacts in the graph are completed.
|
|
@@ -162,10 +156,67 @@ export class ArtifactGraph {
|
|
|
162
156
|
}
|
|
163
157
|
const unmetDeps = artifact.requires.filter(req => !completed.has(req));
|
|
164
158
|
if (unmetDeps.length > 0) {
|
|
165
|
-
blocked[artifact.id] = unmetDeps.sort();
|
|
159
|
+
blocked[artifact.id] = unmetDeps.sort((a, b) => this.compareByDeclarationOrder(a, b));
|
|
166
160
|
}
|
|
167
161
|
}
|
|
168
162
|
return blocked;
|
|
169
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Gets all artifacts that depend on the given artifact (direct and transitive).
|
|
166
|
+
* Returns artifact IDs in reverse topological order (deepest dependents first).
|
|
167
|
+
* Throws an error if a circular dependency is detected.
|
|
168
|
+
*/
|
|
169
|
+
getDependents(artifactId) {
|
|
170
|
+
if (!this.artifacts.has(artifactId)) {
|
|
171
|
+
throw new Error(`Artifact not found: ${artifactId}`);
|
|
172
|
+
}
|
|
173
|
+
const dependents = new Set();
|
|
174
|
+
const visited = new Set();
|
|
175
|
+
const visiting = new Set();
|
|
176
|
+
const dfs = (currentId) => {
|
|
177
|
+
if (visiting.has(currentId)) {
|
|
178
|
+
// Cycle detected
|
|
179
|
+
const cycle = Array.from(visiting).concat(currentId).join(' -> ');
|
|
180
|
+
throw new Error(`Circular dependency detected: ${cycle}`);
|
|
181
|
+
}
|
|
182
|
+
if (visited.has(currentId)) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
visiting.add(currentId);
|
|
186
|
+
// Find all artifacts that depend on currentId
|
|
187
|
+
for (const artifact of this.artifacts.values()) {
|
|
188
|
+
if (artifact.requires.includes(currentId)) {
|
|
189
|
+
dfs(artifact.id);
|
|
190
|
+
dependents.add(artifact.id);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
visiting.delete(currentId);
|
|
194
|
+
visited.add(currentId);
|
|
195
|
+
};
|
|
196
|
+
dfs(artifactId);
|
|
197
|
+
// Convert to array and sort by reverse build order (deepest first)
|
|
198
|
+
const buildOrder = this.getBuildOrder();
|
|
199
|
+
const dependentArray = Array.from(dependents);
|
|
200
|
+
// Sort by reverse position in build order (deepest dependents first)
|
|
201
|
+
// Use declaration order as tiebreaker for artifacts at same depth
|
|
202
|
+
return dependentArray.sort((a, b) => {
|
|
203
|
+
const posA = buildOrder.indexOf(a);
|
|
204
|
+
const posB = buildOrder.indexOf(b);
|
|
205
|
+
if (posA !== posB) {
|
|
206
|
+
return posB - posA; // Reverse order by depth
|
|
207
|
+
}
|
|
208
|
+
// Same depth: use declaration order (already in build order)
|
|
209
|
+
return this.compareByDeclarationOrder(a, b);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Gets reverse topological order for updating artifacts.
|
|
214
|
+
* Returns artifact IDs in the order they should be updated (dependents before dependees).
|
|
215
|
+
* Throws an error if a circular dependency is detected.
|
|
216
|
+
*/
|
|
217
|
+
getReverseUpdateOrder() {
|
|
218
|
+
const buildOrder = this.getBuildOrder();
|
|
219
|
+
return buildOrder.reverse();
|
|
220
|
+
}
|
|
170
221
|
}
|
|
171
222
|
//# sourceMappingURL=graph.js.map
|
|
@@ -2,7 +2,7 @@ export { ArtifactSchema, SchemaYamlSchema, type Artifact, type SchemaYaml, type
|
|
|
2
2
|
export { loadSchema, parseSchema, SchemaValidationError } from './schema.js';
|
|
3
3
|
export { ArtifactGraph } from './graph.js';
|
|
4
4
|
export { detectCompleted } from './state.js';
|
|
5
|
-
export { artifactOutputExists, isGlobPattern, resolveArtifactOutputs } from './outputs.js';
|
|
5
|
+
export { artifactOutputExists, isGlobPattern, resolveArtifactOutputPath, resolveArtifactOutputs, } from './outputs.js';
|
|
6
6
|
export { resolveSchema, listSchemas, listSchemasWithInfo, getSchemaDir, getPackageSchemasDir, getUserSchemasDir, SchemaLoadError, type SchemaInfo, } from './resolver.js';
|
|
7
7
|
export { loadTemplate, loadChangeContext, generateInstructions, formatChangeStatus, TemplateLoadError, type ChangeContext, type LoadChangeContextOptions, type ArtifactInstructions, type DependencyInfo, type ArtifactStatus, type ChangeStatus, type ArtifactPathSummary, } from './instruction-loader.js';
|
|
8
8
|
export type { PlanningHomeSummary, ActionContext, } from '../change-status-policy.js';
|
|
@@ -6,7 +6,7 @@ export { loadSchema, parseSchema, SchemaValidationError } from './schema.js';
|
|
|
6
6
|
export { ArtifactGraph } from './graph.js';
|
|
7
7
|
// State detection
|
|
8
8
|
export { detectCompleted } from './state.js';
|
|
9
|
-
export { artifactOutputExists, isGlobPattern, resolveArtifactOutputs } from './outputs.js';
|
|
9
|
+
export { artifactOutputExists, isGlobPattern, resolveArtifactOutputPath, resolveArtifactOutputs, } from './outputs.js';
|
|
10
10
|
// Schema resolution
|
|
11
11
|
export { resolveSchema, listSchemas, listSchemasWithInfo, getSchemaDir, getPackageSchemasDir, getUserSchemasDir, SchemaLoadError, } from './resolver.js';
|
|
12
12
|
// Instruction loading
|
|
@@ -32,10 +32,18 @@ export interface ChangeContext {
|
|
|
32
32
|
planningHome?: PlanningHome;
|
|
33
33
|
/** Parsed change metadata, when present */
|
|
34
34
|
metadata?: ChangeMetadata;
|
|
35
|
+
/**
|
|
36
|
+
* Artifact IDs counted as complete only because the change declares
|
|
37
|
+
* skip_specs, not because their files exist. Kept separate so status can
|
|
38
|
+
* render them as skipped rather than done.
|
|
39
|
+
*/
|
|
40
|
+
skippedArtifacts?: Set<string>;
|
|
35
41
|
}
|
|
36
42
|
export interface LoadChangeContextOptions {
|
|
37
43
|
changeDir?: string;
|
|
38
44
|
planningHome?: PlanningHome;
|
|
45
|
+
/** Pre-read project config; suppresses schema resolution's fallback config read. */
|
|
46
|
+
projectConfig?: ProjectConfig | null;
|
|
39
47
|
}
|
|
40
48
|
/**
|
|
41
49
|
* Enriched instructions for creating an artifact.
|
|
@@ -73,7 +81,17 @@ export interface ArtifactInstructions {
|
|
|
73
81
|
dependencies: DependencyInfo[];
|
|
74
82
|
/** Artifacts that become available after completing this one */
|
|
75
83
|
unlocks: string[];
|
|
84
|
+
/** True when the change declares skip_specs and this artifact is skipped */
|
|
85
|
+
skipped?: boolean;
|
|
86
|
+
/** Present only when skipped: tells the consumer not to create the artifact */
|
|
87
|
+
warning?: string;
|
|
76
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Warning attached to instructions for an artifact skipped via skip_specs.
|
|
91
|
+
* Carried in the JSON payload too, so agents driving the CLI with --json see
|
|
92
|
+
* the same do-not-create signal as the text output.
|
|
93
|
+
*/
|
|
94
|
+
export declare const SKIP_SPECS_INSTRUCTIONS_WARNING: string;
|
|
77
95
|
/**
|
|
78
96
|
* Dependency information including path and description.
|
|
79
97
|
*/
|
|
@@ -86,6 +104,8 @@ export interface DependencyInfo {
|
|
|
86
104
|
path: string;
|
|
87
105
|
/** Description of the dependency artifact */
|
|
88
106
|
description: string;
|
|
107
|
+
/** True when the dependency is satisfied via skip_specs - no files exist to read */
|
|
108
|
+
skipped?: boolean;
|
|
89
109
|
}
|
|
90
110
|
/**
|
|
91
111
|
* Status of a single artifact in the workflow.
|
|
@@ -95,8 +115,13 @@ export interface ArtifactStatus {
|
|
|
95
115
|
id: string;
|
|
96
116
|
/** Output path pattern */
|
|
97
117
|
outputPath: string;
|
|
98
|
-
/** Status: done, ready, or blocked */
|
|
99
|
-
status: 'done' | 'ready' | 'blocked';
|
|
118
|
+
/** Status: done, skipped (via skip_specs), ready, or blocked */
|
|
119
|
+
status: 'done' | 'skipped' | 'ready' | 'blocked';
|
|
120
|
+
/** Artifact IDs this artifact directly requires (its `requires` edges).
|
|
121
|
+
* Present for every status so callers can compute the transitive required
|
|
122
|
+
* set even when the artifact is already `done` (file-existence status does
|
|
123
|
+
* not imply its dependencies exist). */
|
|
124
|
+
requires: string[];
|
|
100
125
|
/** Missing dependencies (only for blocked) */
|
|
101
126
|
missingDeps?: string[];
|
|
102
127
|
}
|
|
@@ -119,7 +144,9 @@ export interface ChangeStatus {
|
|
|
119
144
|
nextSteps: string[];
|
|
120
145
|
/** Machine-readable action constraints for agents */
|
|
121
146
|
actionContext: ActionContext;
|
|
122
|
-
/** Whether all artifacts are complete */
|
|
147
|
+
/** Whether all planning artifacts are complete */
|
|
148
|
+
isPlanningComplete: boolean;
|
|
149
|
+
/** Compatibility alias for isPlanningComplete */
|
|
123
150
|
isComplete: boolean;
|
|
124
151
|
/** Artifact IDs required before apply phase (from schema's apply.requires) */
|
|
125
152
|
applyRequires: string[];
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import { getSchemaDir, resolveSchema } from './resolver.js';
|
|
3
|
+
import { getSchemaDir, resolveSchema, listSchemasWithInfo } from './resolver.js';
|
|
4
4
|
import { ArtifactGraph } from './graph.js';
|
|
5
5
|
import { detectCompleted } from './state.js';
|
|
6
|
-
import { resolveArtifactOutputs } from './outputs.js';
|
|
6
|
+
import { resolveArtifactOutputPath, resolveArtifactOutputs } from './outputs.js';
|
|
7
7
|
import { readChangeMetadata, resolveSchemaForChange } from '../../utils/change-metadata.js';
|
|
8
8
|
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
9
9
|
import { buildActionContext, buildNextSteps, summarizePlanningHome, } from '../change-status-policy.js';
|
|
@@ -21,6 +21,13 @@ export class TemplateLoadError extends Error {
|
|
|
21
21
|
this.name = 'TemplateLoadError';
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Warning attached to instructions for an artifact skipped via skip_specs.
|
|
26
|
+
* Carried in the JSON payload too, so agents driving the CLI with --json see
|
|
27
|
+
* the same do-not-create signal as the text output.
|
|
28
|
+
*/
|
|
29
|
+
export const SKIP_SPECS_INSTRUCTIONS_WARNING = 'This change declares skip_specs: true in .openspec.yaml (no spec-level behavior changes), so this artifact is skipped.\n' +
|
|
30
|
+
'Do not create spec files - they will conflict with that marker. If requirements now change, remove skip_specs from .openspec.yaml and rerun this command.';
|
|
24
31
|
/**
|
|
25
32
|
* Loads a template from a schema's templates directory.
|
|
26
33
|
*
|
|
@@ -35,7 +42,14 @@ export function loadTemplate(schemaName, templatePath, projectRoot) {
|
|
|
35
42
|
if (!schemaDir) {
|
|
36
43
|
throw new TemplateLoadError(`Schema '${schemaName}' not found`, templatePath);
|
|
37
44
|
}
|
|
38
|
-
const
|
|
45
|
+
const templatesDir = path.join(schemaDir, 'templates');
|
|
46
|
+
const templatePathOnDisk = path.join(templatesDir, templatePath);
|
|
47
|
+
try {
|
|
48
|
+
FileSystemUtils.assertPathWithin(templatesDir, templatePathOnDisk);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
throw new TemplateLoadError(error instanceof Error ? error.message : String(error), templatePathOnDisk);
|
|
52
|
+
}
|
|
39
53
|
if (!fs.existsSync(templatePathOnDisk)) {
|
|
40
54
|
throw new TemplateLoadError(`Template not found: ${templatePathOnDisk}`, templatePathOnDisk);
|
|
41
55
|
}
|
|
@@ -66,10 +80,29 @@ export function loadChangeContext(projectRoot, changeName, schemaName, options =
|
|
|
66
80
|
const metadata = readChangeMetadata(changeDir, projectRoot) ?? undefined;
|
|
67
81
|
const resolvedSchemaName = resolveSchemaForChange(changeDir, schemaName, projectRoot, {
|
|
68
82
|
metadata: metadata ?? null,
|
|
83
|
+
projectConfig: options.projectConfig,
|
|
69
84
|
});
|
|
70
85
|
const schema = resolveSchema(resolvedSchemaName, projectRoot);
|
|
71
86
|
const graph = ArtifactGraph.fromSchema(schema);
|
|
72
87
|
const completed = detectCompleted(graph, changeDir);
|
|
88
|
+
// A change that declares skip_specs has no spec deltas by design, so
|
|
89
|
+
// artifacts generating into specs/ count as complete; otherwise the graph
|
|
90
|
+
// would block their dependents (e.g. tasks) on files that must not exist.
|
|
91
|
+
// Tracked separately so status renders them as skipped, not done.
|
|
92
|
+
const skippedArtifacts = new Set();
|
|
93
|
+
if (metadata?.skip_specs) {
|
|
94
|
+
for (const artifact of graph.getAllArtifacts()) {
|
|
95
|
+
// A schema may write generates as './specs/...' - the globs treat that
|
|
96
|
+
// identically to 'specs/...', so the skip set must too, or validate
|
|
97
|
+
// would honor the marker while instructions tell the agent to create
|
|
98
|
+
// the very files the conflict gate polices.
|
|
99
|
+
const generates = artifact.generates.replace(/^(?:\.\/)+/, '');
|
|
100
|
+
if (generates.startsWith('specs/') && !completed.has(artifact.id)) {
|
|
101
|
+
completed.add(artifact.id);
|
|
102
|
+
skippedArtifacts.add(artifact.id);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
73
106
|
return {
|
|
74
107
|
graph,
|
|
75
108
|
completed,
|
|
@@ -79,6 +112,7 @@ export function loadChangeContext(projectRoot, changeName, schemaName, options =
|
|
|
79
112
|
projectRoot,
|
|
80
113
|
...(options.planningHome ? { planningHome: options.planningHome } : {}),
|
|
81
114
|
...(metadata ? { metadata } : {}),
|
|
115
|
+
...(skippedArtifacts.size > 0 ? { skippedArtifacts } : {}),
|
|
82
116
|
};
|
|
83
117
|
}
|
|
84
118
|
export function generateInstructions(context, artifactId, projectRoot, options = {}) {
|
|
@@ -87,7 +121,7 @@ export function generateInstructions(context, artifactId, projectRoot, options =
|
|
|
87
121
|
throw new Error(`Artifact '${artifactId}' not found in schema '${context.schemaName}'`);
|
|
88
122
|
}
|
|
89
123
|
const templateContent = loadTemplate(context.schemaName, artifact.template, context.projectRoot);
|
|
90
|
-
const dependencies = getDependencyInfo(artifact, context.graph, context.completed);
|
|
124
|
+
const dependencies = getDependencyInfo(artifact, context.graph, context.completed, context.skippedArtifacts);
|
|
91
125
|
const unlocks = getUnlockedArtifacts(context.graph, artifactId);
|
|
92
126
|
// Use projectRoot from context if not explicitly provided
|
|
93
127
|
const effectiveProjectRoot = projectRoot ?? context.projectRoot;
|
|
@@ -101,10 +135,12 @@ export function generateInstructions(context, artifactId, projectRoot, options =
|
|
|
101
135
|
// If config read fails, continue without config
|
|
102
136
|
}
|
|
103
137
|
}
|
|
104
|
-
// Validate rules artifact IDs if config has rules (only once per session)
|
|
138
|
+
// Validate rules artifact IDs if config has rules (only once per session).
|
|
139
|
+
// The rules map is global while each change can use a different schema, so a
|
|
140
|
+
// key is only "unknown" when it matches no artifact in ANY available schema.
|
|
105
141
|
if (projectConfig?.rules) {
|
|
106
|
-
const validArtifactIds = new Set(
|
|
107
|
-
const warnings = validateConfigRules(projectConfig.rules, validArtifactIds
|
|
142
|
+
const validArtifactIds = new Set(listSchemasWithInfo(effectiveProjectRoot ?? undefined).flatMap((s) => s.artifacts));
|
|
143
|
+
const warnings = validateConfigRules(projectConfig.rules, validArtifactIds);
|
|
108
144
|
// Show each unique warning only once per session
|
|
109
145
|
for (const warning of warnings) {
|
|
110
146
|
if (!shownWarnings.has(warning)) {
|
|
@@ -115,7 +151,9 @@ export function generateInstructions(context, artifactId, projectRoot, options =
|
|
|
115
151
|
}
|
|
116
152
|
// Extract context and rules as separate fields (not prepended to template)
|
|
117
153
|
const configContext = projectConfig?.context?.trim() || undefined;
|
|
118
|
-
const rulesForArtifact = projectConfig?.rules
|
|
154
|
+
const rulesForArtifact = projectConfig?.rules && Object.hasOwn(projectConfig.rules, artifactId)
|
|
155
|
+
? projectConfig.rules[artifactId]
|
|
156
|
+
: undefined;
|
|
119
157
|
const configRules = rulesForArtifact && rulesForArtifact.length > 0 ? rulesForArtifact : undefined;
|
|
120
158
|
return {
|
|
121
159
|
changeName: context.changeName,
|
|
@@ -124,13 +162,16 @@ export function generateInstructions(context, artifactId, projectRoot, options =
|
|
|
124
162
|
changeDir: context.changeDir,
|
|
125
163
|
planningHome: summarizePlanningHome(context.planningHome),
|
|
126
164
|
outputPath: artifact.generates,
|
|
127
|
-
resolvedOutputPath:
|
|
165
|
+
resolvedOutputPath: resolveArtifactOutputPath(context.changeDir, artifact.generates),
|
|
128
166
|
existingOutputPaths: resolveArtifactOutputs(context.changeDir, artifact.generates),
|
|
129
167
|
description: artifact.description,
|
|
130
168
|
instruction: artifact.instruction,
|
|
131
169
|
context: configContext,
|
|
132
170
|
rules: configRules,
|
|
133
171
|
...(options.references !== undefined ? { references: options.references } : {}),
|
|
172
|
+
...(context.skippedArtifacts?.has(artifact.id)
|
|
173
|
+
? { skipped: true, warning: SKIP_SPECS_INSTRUCTIONS_WARNING }
|
|
174
|
+
: {}),
|
|
134
175
|
template: templateContent,
|
|
135
176
|
dependencies,
|
|
136
177
|
unlocks,
|
|
@@ -139,7 +180,7 @@ export function generateInstructions(context, artifactId, projectRoot, options =
|
|
|
139
180
|
/**
|
|
140
181
|
* Gets dependency info including paths and descriptions.
|
|
141
182
|
*/
|
|
142
|
-
function getDependencyInfo(artifact, graph, completed) {
|
|
183
|
+
function getDependencyInfo(artifact, graph, completed, skippedArtifacts) {
|
|
143
184
|
return artifact.requires.map(id => {
|
|
144
185
|
const depArtifact = graph.getArtifact(id);
|
|
145
186
|
return {
|
|
@@ -147,11 +188,16 @@ function getDependencyInfo(artifact, graph, completed) {
|
|
|
147
188
|
done: completed.has(id),
|
|
148
189
|
path: depArtifact?.generates ?? id,
|
|
149
190
|
description: depArtifact?.description ?? '',
|
|
191
|
+
...(skippedArtifacts?.has(id) ? { skipped: true } : {}),
|
|
150
192
|
};
|
|
151
193
|
});
|
|
152
194
|
}
|
|
153
195
|
/**
|
|
154
196
|
* Gets artifacts that become available after completing the given artifact.
|
|
197
|
+
*
|
|
198
|
+
* `getAllArtifacts()` already yields the schema's declaration order, so the list
|
|
199
|
+
* is returned as collected: sorting it alphabetically would have `unlocks` name
|
|
200
|
+
* the artifacts in a different order than `status` recommends them.
|
|
155
201
|
*/
|
|
156
202
|
function getUnlockedArtifacts(graph, artifactId) {
|
|
157
203
|
const unlocks = [];
|
|
@@ -160,7 +206,7 @@ function getUnlockedArtifacts(graph, artifactId) {
|
|
|
160
206
|
unlocks.push(artifact.id);
|
|
161
207
|
}
|
|
162
208
|
}
|
|
163
|
-
return unlocks
|
|
209
|
+
return unlocks;
|
|
164
210
|
}
|
|
165
211
|
/**
|
|
166
212
|
* Formats the status of all artifacts in a change.
|
|
@@ -179,14 +225,23 @@ export function formatChangeStatus(context, options = {}) {
|
|
|
179
225
|
const artifactStatuses = artifacts.map(artifact => {
|
|
180
226
|
artifactPaths[artifact.id] = {
|
|
181
227
|
outputPath: artifact.generates,
|
|
182
|
-
resolvedOutputPath:
|
|
228
|
+
resolvedOutputPath: resolveArtifactOutputPath(context.changeDir, artifact.generates),
|
|
183
229
|
existingOutputPaths: resolveArtifactOutputs(context.changeDir, artifact.generates),
|
|
184
230
|
};
|
|
231
|
+
if (context.skippedArtifacts?.has(artifact.id)) {
|
|
232
|
+
return {
|
|
233
|
+
id: artifact.id,
|
|
234
|
+
outputPath: artifact.generates,
|
|
235
|
+
status: 'skipped',
|
|
236
|
+
requires: artifact.requires,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
185
239
|
if (context.completed.has(artifact.id)) {
|
|
186
240
|
return {
|
|
187
241
|
id: artifact.id,
|
|
188
242
|
outputPath: artifact.generates,
|
|
189
243
|
status: 'done',
|
|
244
|
+
requires: artifact.requires,
|
|
190
245
|
};
|
|
191
246
|
}
|
|
192
247
|
if (ready.has(artifact.id)) {
|
|
@@ -194,12 +249,14 @@ export function formatChangeStatus(context, options = {}) {
|
|
|
194
249
|
id: artifact.id,
|
|
195
250
|
outputPath: artifact.generates,
|
|
196
251
|
status: 'ready',
|
|
252
|
+
requires: artifact.requires,
|
|
197
253
|
};
|
|
198
254
|
}
|
|
199
255
|
return {
|
|
200
256
|
id: artifact.id,
|
|
201
257
|
outputPath: artifact.generates,
|
|
202
258
|
status: 'blocked',
|
|
259
|
+
requires: artifact.requires,
|
|
203
260
|
missingDeps: blocked[artifact.id] ?? [],
|
|
204
261
|
};
|
|
205
262
|
});
|
|
@@ -215,6 +272,7 @@ export function formatChangeStatus(context, options = {}) {
|
|
|
215
272
|
planningHome: summarizePlanningHome(context.planningHome),
|
|
216
273
|
changeRoot: context.changeDir,
|
|
217
274
|
artifactPaths,
|
|
275
|
+
isPlanningComplete: isComplete,
|
|
218
276
|
isComplete,
|
|
219
277
|
applyRequires,
|
|
220
278
|
nextSteps: buildNextSteps({
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Checks if a path contains glob pattern characters.
|
|
3
3
|
*/
|
|
4
4
|
export declare function isGlobPattern(pattern: string): boolean;
|
|
5
|
+
export declare function resolveArtifactOutputPath(changeDir: string, generates: string): string;
|
|
5
6
|
/**
|
|
6
7
|
* Resolves an artifact's output path(s) to concrete files that currently exist.
|
|
7
8
|
* Returns absolute file paths. Glob matches are sorted for deterministic output.
|
|
@@ -8,16 +8,66 @@ import { FileSystemUtils } from '../../utils/file-system.js';
|
|
|
8
8
|
export function isGlobPattern(pattern) {
|
|
9
9
|
return pattern.includes('*') || pattern.includes('?') || pattern.includes('[');
|
|
10
10
|
}
|
|
11
|
+
export function resolveArtifactOutputPath(changeDir, generates) {
|
|
12
|
+
const outputPath = path.join(changeDir, generates);
|
|
13
|
+
FileSystemUtils.assertPathWithin(changeDir, outputPath);
|
|
14
|
+
return outputPath;
|
|
15
|
+
}
|
|
16
|
+
function assertGlobDirectoryTraversal(changeDir, currentDir, directorySegments, segmentIndex = 0, visited = new Set(), canonicalChangeDir = FileSystemUtils.canonicalizeExistingPath(changeDir), ancestors = new Set()) {
|
|
17
|
+
if (segmentIndex >= directorySegments.length)
|
|
18
|
+
return;
|
|
19
|
+
const canonicalDir = FileSystemUtils.canonicalizeExistingPath(currentDir);
|
|
20
|
+
FileSystemUtils.assertPathWithin(canonicalChangeDir, canonicalDir);
|
|
21
|
+
const visitKey = `${canonicalDir}\0${segmentIndex}`;
|
|
22
|
+
if (ancestors.has(visitKey)) {
|
|
23
|
+
throw new Error(`Cannot resolve artifact outputs through a linked directory cycle: ${currentDir}`);
|
|
24
|
+
}
|
|
25
|
+
if (visited.has(visitKey))
|
|
26
|
+
return;
|
|
27
|
+
visited.add(visitKey);
|
|
28
|
+
ancestors.add(visitKey);
|
|
29
|
+
try {
|
|
30
|
+
const segment = directorySegments[segmentIndex];
|
|
31
|
+
if (segment === '**') {
|
|
32
|
+
// `**` may consume no directory at all.
|
|
33
|
+
assertGlobDirectoryTraversal(changeDir, canonicalDir, directorySegments, segmentIndex + 1, visited, canonicalChangeDir, ancestors);
|
|
34
|
+
}
|
|
35
|
+
const matches = fg.sync(segment === '**' ? '*' : segment, {
|
|
36
|
+
cwd: canonicalDir,
|
|
37
|
+
onlyFiles: false,
|
|
38
|
+
followSymbolicLinks: false,
|
|
39
|
+
deep: 1,
|
|
40
|
+
});
|
|
41
|
+
for (const match of matches) {
|
|
42
|
+
const candidate = path.join(canonicalDir, match);
|
|
43
|
+
try {
|
|
44
|
+
if (!fs.statSync(candidate).isDirectory())
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error.code === 'ENOENT')
|
|
49
|
+
continue;
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
const canonicalCandidate = FileSystemUtils.canonicalizeExistingPath(candidate);
|
|
53
|
+
FileSystemUtils.assertPathWithin(canonicalChangeDir, canonicalCandidate);
|
|
54
|
+
assertGlobDirectoryTraversal(changeDir, canonicalCandidate, directorySegments, segment === '**' ? segmentIndex : segmentIndex + 1, visited, canonicalChangeDir, ancestors);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
ancestors.delete(visitKey);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
11
61
|
/**
|
|
12
62
|
* Resolves an artifact's output path(s) to concrete files that currently exist.
|
|
13
63
|
* Returns absolute file paths. Glob matches are sorted for deterministic output.
|
|
14
64
|
*/
|
|
15
65
|
export function resolveArtifactOutputs(changeDir, generates) {
|
|
66
|
+
const outputPath = resolveArtifactOutputPath(changeDir, generates);
|
|
16
67
|
if (!isGlobPattern(generates)) {
|
|
17
|
-
const fullPath = path.join(changeDir, generates);
|
|
18
68
|
try {
|
|
19
|
-
return fs.statSync(
|
|
20
|
-
? [FileSystemUtils.canonicalizeExistingPath(
|
|
69
|
+
return fs.statSync(outputPath).isFile()
|
|
70
|
+
? [FileSystemUtils.canonicalizeExistingPath(outputPath)]
|
|
21
71
|
: [];
|
|
22
72
|
}
|
|
23
73
|
catch {
|
|
@@ -25,9 +75,21 @@ export function resolveArtifactOutputs(changeDir, generates) {
|
|
|
25
75
|
}
|
|
26
76
|
}
|
|
27
77
|
const normalizedPattern = FileSystemUtils.toPosixPath(generates);
|
|
78
|
+
assertGlobDirectoryTraversal(changeDir, changeDir, normalizedPattern.split('/').slice(0, -1));
|
|
28
79
|
const matches = fg
|
|
29
|
-
.sync(normalizedPattern, {
|
|
30
|
-
|
|
80
|
+
.sync(normalizedPattern, {
|
|
81
|
+
cwd: changeDir,
|
|
82
|
+
onlyFiles: true,
|
|
83
|
+
absolute: true,
|
|
84
|
+
// Preserve existing support for linked artifact directories. Every
|
|
85
|
+
// concrete match is canonically confined below before it is returned.
|
|
86
|
+
followSymbolicLinks: true,
|
|
87
|
+
})
|
|
88
|
+
.map((match) => {
|
|
89
|
+
const normalizedMatch = path.normalize(match);
|
|
90
|
+
FileSystemUtils.assertPathWithin(changeDir, normalizedMatch);
|
|
91
|
+
return FileSystemUtils.canonicalizeExistingPath(normalizedMatch);
|
|
92
|
+
});
|
|
31
93
|
return Array.from(new Set(matches)).sort();
|
|
32
94
|
}
|
|
33
95
|
/**
|