@seanmars/tospec 0.19.0-beta.7 → 0.20.0
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/CHANGELOG.md +60 -231
- package/README.md +69 -82
- package/assets/dashboard/app.js +14 -3
- package/assets/dashboard/style.css +7 -0
- package/assets/rules/tospec/decision.md +3 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +227 -86
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/config.d.ts +9 -17
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +366 -107
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/dashboard.d.ts +57 -99
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dashboard.js +224 -282
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/decision.d.ts +41 -19
- package/dist/commands/decision.d.ts.map +1 -1
- package/dist/commands/decision.js +385 -79
- package/dist/commands/decision.js.map +1 -1
- package/dist/commands/metrics.d.ts +28 -51
- package/dist/commands/metrics.d.ts.map +1 -1
- package/dist/commands/metrics.js +62 -93
- package/dist/commands/metrics.js.map +1 -1
- package/dist/commands/shared-output.d.ts +18 -21
- package/dist/commands/shared-output.d.ts.map +1 -1
- package/dist/commands/shared-output.js +49 -23
- package/dist/commands/shared-output.js.map +1 -1
- package/dist/commands/show.d.ts +7 -0
- package/dist/commands/show.d.ts.map +1 -1
- package/dist/commands/show.js +29 -7
- package/dist/commands/show.js.map +1 -1
- package/dist/commands/validate.d.ts +43 -45
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +262 -129
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/workflow/index.d.ts +6 -10
- package/dist/commands/workflow/index.d.ts.map +1 -1
- package/dist/commands/workflow/index.js +6 -10
- package/dist/commands/workflow/index.js.map +1 -1
- package/dist/commands/workflow/instructions.d.ts +21 -8
- package/dist/commands/workflow/instructions.d.ts.map +1 -1
- package/dist/commands/workflow/instructions.js +254 -95
- package/dist/commands/workflow/instructions.js.map +1 -1
- package/dist/commands/workflow/new-change.d.ts +4 -5
- package/dist/commands/workflow/new-change.d.ts.map +1 -1
- package/dist/commands/workflow/new-change.js +90 -25
- package/dist/commands/workflow/new-change.js.map +1 -1
- package/dist/commands/workflow/schemas.d.ts +3 -5
- package/dist/commands/workflow/schemas.d.ts.map +1 -1
- package/dist/commands/workflow/schemas.js +37 -11
- package/dist/commands/workflow/schemas.js.map +1 -1
- package/dist/commands/workflow/shared.d.ts +48 -21
- package/dist/commands/workflow/shared.d.ts.map +1 -1
- package/dist/commands/workflow/shared.js +36 -33
- package/dist/commands/workflow/shared.js.map +1 -1
- package/dist/commands/workflow/status.d.ts +10 -6
- package/dist/commands/workflow/status.d.ts.map +1 -1
- package/dist/commands/workflow/status.js +81 -42
- package/dist/commands/workflow/status.js.map +1 -1
- package/dist/commands/workflow/templates.d.ts +10 -3
- package/dist/commands/workflow/templates.d.ts.map +1 -1
- package/dist/commands/workflow/templates.js +39 -40
- package/dist/commands/workflow/templates.js.map +1 -1
- package/dist/core/archive.d.ts +26 -21
- package/dist/core/archive.d.ts.map +1 -1
- package/dist/core/archive.js +378 -235
- package/dist/core/archive.js.map +1 -1
- package/dist/core/artifact-graph/graph.d.ts +25 -42
- package/dist/core/artifact-graph/graph.d.ts.map +1 -1
- package/dist/core/artifact-graph/graph.js +45 -63
- package/dist/core/artifact-graph/graph.js.map +1 -1
- package/dist/core/artifact-graph/index.d.ts +1 -1
- package/dist/core/artifact-graph/index.d.ts.map +1 -1
- package/dist/core/artifact-graph/index.js +1 -1
- package/dist/core/artifact-graph/index.js.map +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +97 -103
- package/dist/core/artifact-graph/instruction-loader.d.ts.map +1 -1
- package/dist/core/artifact-graph/instruction-loader.js +146 -119
- package/dist/core/artifact-graph/instruction-loader.js.map +1 -1
- package/dist/core/artifact-graph/outputs.d.ts +9 -23
- package/dist/core/artifact-graph/outputs.d.ts.map +1 -1
- package/dist/core/artifact-graph/outputs.js +45 -38
- package/dist/core/artifact-graph/outputs.js.map +1 -1
- package/dist/core/artifact-graph/resolver.d.ts +44 -63
- package/dist/core/artifact-graph/resolver.d.ts.map +1 -1
- package/dist/core/artifact-graph/resolver.js +85 -86
- package/dist/core/artifact-graph/resolver.js.map +1 -1
- package/dist/core/artifact-graph/schema.d.ts +0 -6
- package/dist/core/artifact-graph/schema.d.ts.map +1 -1
- package/dist/core/artifact-graph/schema.js +7 -32
- package/dist/core/artifact-graph/schema.js.map +1 -1
- package/dist/core/artifact-graph/state.d.ts +1 -8
- package/dist/core/artifact-graph/state.d.ts.map +1 -1
- package/dist/core/artifact-graph/state.js +2 -17
- package/dist/core/artifact-graph/state.js.map +1 -1
- package/dist/core/artifact-graph/stub-detection.d.ts +12 -0
- package/dist/core/artifact-graph/stub-detection.d.ts.map +1 -0
- package/dist/core/artifact-graph/stub-detection.js +39 -0
- package/dist/core/artifact-graph/stub-detection.js.map +1 -0
- package/dist/core/artifact-graph/types.d.ts +4 -0
- package/dist/core/artifact-graph/types.d.ts.map +1 -1
- package/dist/core/artifact-graph/types.js +30 -10
- package/dist/core/artifact-graph/types.js.map +1 -1
- package/dist/core/available-tools.d.ts +3 -12
- package/dist/core/available-tools.d.ts.map +1 -1
- package/dist/core/available-tools.js +4 -13
- package/dist/core/available-tools.js.map +1 -1
- package/dist/core/change-metadata/schema.d.ts +1 -1
- package/dist/core/change-metadata/schema.d.ts.map +1 -1
- package/dist/core/change-metadata/schema.js +10 -7
- package/dist/core/change-metadata/schema.js.map +1 -1
- package/dist/core/change-presenter.d.ts +23 -18
- package/dist/core/change-presenter.d.ts.map +1 -1
- package/dist/core/change-presenter.js +102 -43
- package/dist/core/change-presenter.js.map +1 -1
- package/dist/core/change-status-policy.d.ts +8 -1
- package/dist/core/change-status-policy.d.ts.map +1 -1
- package/dist/core/change-status-policy.js +25 -1
- package/dist/core/change-status-policy.js.map +1 -1
- package/dist/core/codex-metrics.d.ts +25 -45
- package/dist/core/codex-metrics.d.ts.map +1 -1
- package/dist/core/codex-metrics.js +44 -88
- package/dist/core/codex-metrics.js.map +1 -1
- package/dist/core/codex-residue.d.ts +14 -15
- package/dist/core/codex-residue.d.ts.map +1 -1
- package/dist/core/codex-residue.js +18 -22
- package/dist/core/codex-residue.js.map +1 -1
- package/dist/core/command-generation/adapters/claude.d.ts +2 -9
- package/dist/core/command-generation/adapters/claude.d.ts.map +1 -1
- package/dist/core/command-generation/adapters/claude.js +2 -12
- package/dist/core/command-generation/adapters/claude.js.map +1 -1
- package/dist/core/command-generation/adapters/index.d.ts +1 -9
- package/dist/core/command-generation/adapters/index.d.ts.map +1 -1
- package/dist/core/command-generation/adapters/index.js +1 -9
- package/dist/core/command-generation/adapters/index.js.map +1 -1
- package/dist/core/command-generation/generator.d.ts +0 -17
- package/dist/core/command-generation/generator.d.ts.map +1 -1
- package/dist/core/command-generation/generator.js +0 -17
- package/dist/core/command-generation/generator.js.map +1 -1
- package/dist/core/command-generation/index.d.ts +2 -5
- package/dist/core/command-generation/index.d.ts.map +1 -1
- package/dist/core/command-generation/index.js +0 -9
- package/dist/core/command-generation/index.js.map +1 -1
- package/dist/core/command-generation/types.d.ts +10 -36
- package/dist/core/command-generation/types.d.ts.map +1 -1
- package/dist/core/command-generation/types.js +0 -6
- package/dist/core/command-generation/types.js.map +1 -1
- package/dist/core/command-generation/yaml.d.ts +3 -18
- package/dist/core/command-generation/yaml.d.ts.map +1 -1
- package/dist/core/command-generation/yaml.js +5 -23
- package/dist/core/command-generation/yaml.js.map +1 -1
- package/dist/core/config-prompts.d.ts +2 -4
- package/dist/core/config-prompts.d.ts.map +1 -1
- package/dist/core/config-prompts.js +2 -7
- package/dist/core/config-prompts.js.map +1 -1
- package/dist/core/config-schema.d.ts +8 -53
- package/dist/core/config-schema.d.ts.map +1 -1
- package/dist/core/config-schema.js +49 -62
- package/dist/core/config-schema.js.map +1 -1
- package/dist/core/config.d.ts +39 -26
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +36 -22
- package/dist/core/config.js.map +1 -1
- package/dist/core/dashboard-activity.d.ts +7 -9
- package/dist/core/dashboard-activity.d.ts.map +1 -1
- package/dist/core/dashboard-activity.js +26 -24
- package/dist/core/dashboard-activity.js.map +1 -1
- package/dist/core/dashboard-data.d.ts +40 -22
- package/dist/core/dashboard-data.d.ts.map +1 -1
- package/dist/core/dashboard-data.js +62 -70
- package/dist/core/dashboard-data.js.map +1 -1
- package/dist/core/global-config.d.ts +24 -53
- package/dist/core/global-config.d.ts.map +1 -1
- package/dist/core/global-config.js +38 -67
- package/dist/core/global-config.js.map +1 -1
- package/dist/core/init.d.ts +25 -19
- package/dist/core/init.d.ts.map +1 -1
- package/dist/core/init.js +168 -181
- package/dist/core/init.js.map +1 -1
- package/dist/core/list.d.ts +1 -1
- package/dist/core/list.d.ts.map +1 -1
- package/dist/core/list.js +121 -28
- package/dist/core/list.js.map +1 -1
- package/dist/core/local-server.d.ts +41 -83
- package/dist/core/local-server.d.ts.map +1 -1
- package/dist/core/local-server.js +53 -98
- package/dist/core/local-server.js.map +1 -1
- package/dist/core/markdown-render.d.ts +25 -0
- package/dist/core/markdown-render.d.ts.map +1 -0
- package/dist/core/markdown-render.js +94 -0
- package/dist/core/markdown-render.js.map +1 -0
- package/dist/core/migrate.d.ts +29 -14
- package/dist/core/migrate.d.ts.map +1 -1
- package/dist/core/migrate.js +184 -122
- package/dist/core/migrate.js.map +1 -1
- package/dist/core/parsers/change-parser.d.ts +7 -10
- package/dist/core/parsers/change-parser.d.ts.map +1 -1
- package/dist/core/parsers/change-parser.js +48 -56
- package/dist/core/parsers/change-parser.js.map +1 -1
- package/dist/core/parsers/markdown-parser.d.ts +8 -9
- package/dist/core/parsers/markdown-parser.d.ts.map +1 -1
- package/dist/core/parsers/markdown-parser.js +31 -22
- package/dist/core/parsers/markdown-parser.js.map +1 -1
- package/dist/core/parsers/requirement-blocks.d.ts +43 -15
- package/dist/core/parsers/requirement-blocks.d.ts.map +1 -1
- package/dist/core/parsers/requirement-blocks.js +142 -70
- package/dist/core/parsers/requirement-blocks.js.map +1 -1
- package/dist/core/parsers/requirement-text.d.ts +73 -79
- package/dist/core/parsers/requirement-text.d.ts.map +1 -1
- package/dist/core/parsers/requirement-text.js +137 -79
- package/dist/core/parsers/requirement-text.js.map +1 -1
- package/dist/core/parsers/spec-structure.d.ts.map +1 -1
- package/dist/core/parsers/spec-structure.js +10 -6
- package/dist/core/parsers/spec-structure.js.map +1 -1
- package/dist/core/profiles.d.ts +3 -10
- package/dist/core/profiles.d.ts.map +1 -1
- package/dist/core/profiles.js +5 -12
- package/dist/core/profiles.js.map +1 -1
- package/dist/core/project-config.d.ts +43 -44
- package/dist/core/project-config.d.ts.map +1 -1
- package/dist/core/project-config.js +107 -82
- package/dist/core/project-config.js.map +1 -1
- package/dist/core/project-layout.d.ts +9 -17
- package/dist/core/project-layout.d.ts.map +1 -1
- package/dist/core/project-layout.js +16 -26
- package/dist/core/project-layout.js.map +1 -1
- package/dist/core/root-selection.d.ts +11 -7
- package/dist/core/root-selection.d.ts.map +1 -1
- package/dist/core/root-selection.js +7 -8
- package/dist/core/root-selection.js.map +1 -1
- package/dist/core/rules.d.ts +6 -1
- package/dist/core/rules.d.ts.map +1 -1
- package/dist/core/rules.js +23 -2
- package/dist/core/rules.js.map +1 -1
- package/dist/core/schema-names.d.ts +16 -0
- package/dist/core/schema-names.d.ts.map +1 -0
- package/dist/core/schema-names.js +16 -0
- package/dist/core/schema-names.js.map +1 -0
- package/dist/core/schemas/base.schema.d.ts +3 -0
- package/dist/core/schemas/base.schema.d.ts.map +1 -1
- package/dist/core/schemas/base.schema.js +22 -6
- package/dist/core/schemas/base.schema.js.map +1 -1
- package/dist/core/schemas/change.schema.d.ts +16 -0
- package/dist/core/schemas/change.schema.d.ts.map +1 -1
- package/dist/core/schemas/change.schema.js +41 -10
- package/dist/core/schemas/change.schema.js.map +1 -1
- package/dist/core/schemas/spec.schema.d.ts +2 -0
- package/dist/core/schemas/spec.schema.d.ts.map +1 -1
- package/dist/core/shared/index.d.ts +3 -8
- package/dist/core/shared/index.d.ts.map +1 -1
- package/dist/core/shared/index.js +3 -8
- package/dist/core/shared/index.js.map +1 -1
- package/dist/core/shared/rules-generation.d.ts +19 -8
- package/dist/core/shared/rules-generation.d.ts.map +1 -1
- package/dist/core/shared/rules-generation.js +117 -50
- package/dist/core/shared/rules-generation.js.map +1 -1
- package/dist/core/shared/skill-generation.d.ts +28 -43
- package/dist/core/shared/skill-generation.d.ts.map +1 -1
- package/dist/core/shared/skill-generation.js +82 -51
- package/dist/core/shared/skill-generation.js.map +1 -1
- package/dist/core/shared/tool-detection.d.ts +39 -61
- package/dist/core/shared/tool-detection.d.ts.map +1 -1
- package/dist/core/shared/tool-detection.js +88 -80
- package/dist/core/shared/tool-detection.js.map +1 -1
- package/dist/core/skill-metrics.d.ts +36 -63
- package/dist/core/skill-metrics.d.ts.map +1 -1
- package/dist/core/skill-metrics.js +34 -73
- package/dist/core/skill-metrics.js.map +1 -1
- package/dist/core/spec-presenter.js +6 -6
- package/dist/core/spec-presenter.js.map +1 -1
- package/dist/core/specs-apply.d.ts +22 -23
- package/dist/core/specs-apply.d.ts.map +1 -1
- package/dist/core/specs-apply.js +166 -193
- package/dist/core/specs-apply.js.map +1 -1
- package/dist/core/templates/fragments/interview.d.ts +2 -6
- package/dist/core/templates/fragments/interview.d.ts.map +1 -1
- package/dist/core/templates/fragments/interview.js +2 -6
- package/dist/core/templates/fragments/interview.js.map +1 -1
- package/dist/core/templates/fragments/next-step.d.ts +4 -8
- package/dist/core/templates/fragments/next-step.d.ts.map +1 -1
- package/dist/core/templates/fragments/next-step.js +4 -8
- package/dist/core/templates/fragments/next-step.js.map +1 -1
- package/dist/core/templates/fragments/validate.d.ts +13 -0
- package/dist/core/templates/fragments/validate.d.ts.map +1 -0
- package/dist/core/templates/fragments/validate.js +13 -0
- package/dist/core/templates/fragments/validate.js.map +1 -0
- package/dist/core/templates/fragments/verify.d.ts +9 -12
- package/dist/core/templates/fragments/verify.d.ts.map +1 -1
- package/dist/core/templates/fragments/verify.js +9 -12
- package/dist/core/templates/fragments/verify.js.map +1 -1
- package/dist/core/templates/index.d.ts +0 -6
- package/dist/core/templates/index.d.ts.map +1 -1
- package/dist/core/templates/index.js +0 -7
- package/dist/core/templates/index.js.map +1 -1
- package/dist/core/templates/skill-templates.d.ts +1 -5
- package/dist/core/templates/skill-templates.d.ts.map +1 -1
- package/dist/core/templates/skill-templates.js +0 -5
- package/dist/core/templates/skill-templates.js.map +1 -1
- package/dist/core/templates/types.d.ts +3 -7
- package/dist/core/templates/types.d.ts.map +1 -1
- package/dist/core/templates/types.js +0 -3
- package/dist/core/templates/types.js.map +1 -1
- package/dist/core/templates/workflows/apply.d.ts +3 -9
- package/dist/core/templates/workflows/apply.d.ts.map +1 -1
- package/dist/core/templates/workflows/apply.js +11 -13
- package/dist/core/templates/workflows/apply.js.map +1 -1
- package/dist/core/templates/workflows/archive.d.ts +0 -6
- package/dist/core/templates/workflows/archive.d.ts.map +1 -1
- package/dist/core/templates/workflows/archive.js +19 -6
- package/dist/core/templates/workflows/archive.js.map +1 -1
- package/dist/core/templates/workflows/decision.js +4 -4
- package/dist/core/templates/workflows/decision.js.map +1 -1
- package/dist/core/templates/workflows/explore.js +1 -1
- package/dist/core/templates/workflows/grill.d.ts.map +1 -1
- package/dist/core/templates/workflows/grill.js +0 -2
- package/dist/core/templates/workflows/grill.js.map +1 -1
- package/dist/core/templates/workflows/issue.d.ts +0 -6
- package/dist/core/templates/workflows/issue.d.ts.map +1 -1
- package/dist/core/templates/workflows/issue.js +3 -2
- package/dist/core/templates/workflows/issue.js.map +1 -1
- package/dist/core/templates/workflows/propose.d.ts +0 -6
- package/dist/core/templates/workflows/propose.d.ts.map +1 -1
- package/dist/core/templates/workflows/propose.js +2 -2
- package/dist/core/templates/workflows/propose.js.map +1 -1
- package/dist/core/templates/workflows/sync.d.ts +2 -8
- package/dist/core/templates/workflows/sync.d.ts.map +1 -1
- package/dist/core/templates/workflows/sync.js +4 -3
- package/dist/core/templates/workflows/sync.js.map +1 -1
- package/dist/core/templates/workflows/update.d.ts +0 -6
- package/dist/core/templates/workflows/update.d.ts.map +1 -1
- package/dist/core/templates/workflows/update.js +9 -2
- package/dist/core/templates/workflows/update.js.map +1 -1
- package/dist/core/update.d.ts +23 -23
- package/dist/core/update.d.ts.map +1 -1
- package/dist/core/update.js +139 -118
- package/dist/core/update.js.map +1 -1
- package/dist/core/user-state-migration.d.ts +13 -15
- package/dist/core/user-state-migration.d.ts.map +1 -1
- package/dist/core/user-state-migration.js +16 -20
- package/dist/core/user-state-migration.js.map +1 -1
- package/dist/core/validation/constants.d.ts +4 -10
- package/dist/core/validation/constants.d.ts.map +1 -1
- package/dist/core/validation/constants.js +25 -20
- package/dist/core/validation/constants.js.map +1 -1
- package/dist/core/validation/prose-length.d.ts +15 -0
- package/dist/core/validation/prose-length.d.ts.map +1 -0
- package/dist/core/validation/prose-length.js +29 -0
- package/dist/core/validation/prose-length.js.map +1 -0
- package/dist/core/validation/purpose-placeholder.d.ts +9 -16
- package/dist/core/validation/purpose-placeholder.d.ts.map +1 -1
- package/dist/core/validation/purpose-placeholder.js +30 -44
- package/dist/core/validation/purpose-placeholder.js.map +1 -1
- package/dist/core/validation/section-validator.d.ts +4 -4
- package/dist/core/validation/section-validator.d.ts.map +1 -1
- package/dist/core/validation/section-validator.js +43 -7
- package/dist/core/validation/section-validator.js.map +1 -1
- package/dist/core/validation/task-numbering.d.ts +6 -3
- package/dist/core/validation/task-numbering.d.ts.map +1 -1
- package/dist/core/validation/task-numbering.js +23 -11
- package/dist/core/validation/task-numbering.js.map +1 -1
- package/dist/core/validation/types.d.ts +18 -0
- package/dist/core/validation/types.d.ts.map +1 -1
- package/dist/core/validation/types.js +12 -1
- package/dist/core/validation/types.js.map +1 -1
- package/dist/core/validation/validator.d.ts +43 -63
- package/dist/core/validation/validator.d.ts.map +1 -1
- package/dist/core/validation/validator.js +500 -267
- package/dist/core/validation/validator.js.map +1 -1
- package/dist/prompts/searchable-multi-select.d.ts +3 -8
- package/dist/prompts/searchable-multi-select.d.ts.map +1 -1
- package/dist/prompts/searchable-multi-select.js +16 -39
- package/dist/prompts/searchable-multi-select.js.map +1 -1
- package/dist/utils/change-metadata.d.ts +11 -50
- package/dist/utils/change-metadata.d.ts.map +1 -1
- package/dist/utils/change-metadata.js +48 -67
- package/dist/utils/change-metadata.js.map +1 -1
- package/dist/utils/change-utils.d.ts +30 -54
- package/dist/utils/change-utils.d.ts.map +1 -1
- package/dist/utils/change-utils.js +133 -91
- package/dist/utils/change-utils.js.map +1 -1
- package/dist/utils/file-lock.d.ts +39 -0
- package/dist/utils/file-lock.d.ts.map +1 -0
- package/dist/utils/file-lock.js +149 -0
- package/dist/utils/file-lock.js.map +1 -0
- package/dist/utils/file-system.d.ts +12 -32
- package/dist/utils/file-system.d.ts.map +1 -1
- package/dist/utils/file-system.js +16 -40
- package/dist/utils/file-system.js.map +1 -1
- package/dist/utils/frontmatter.d.ts +7 -11
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +11 -11
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/interactive.d.ts +4 -9
- package/dist/utils/interactive.d.ts.map +1 -1
- package/dist/utils/interactive.js +2 -4
- package/dist/utils/interactive.js.map +1 -1
- package/dist/utils/item-discovery.d.ts +15 -10
- package/dist/utils/item-discovery.d.ts.map +1 -1
- package/dist/utils/item-discovery.js +42 -47
- package/dist/utils/item-discovery.js.map +1 -1
- package/dist/utils/link.d.ts +9 -18
- package/dist/utils/link.d.ts.map +1 -1
- package/dist/utils/link.js +9 -18
- package/dist/utils/link.js.map +1 -1
- package/dist/utils/requirement-diff.d.ts +13 -23
- package/dist/utils/requirement-diff.d.ts.map +1 -1
- package/dist/utils/requirement-diff.js +13 -23
- package/dist/utils/requirement-diff.js.map +1 -1
- package/dist/utils/spec-files.d.ts +7 -20
- package/dist/utils/spec-files.d.ts.map +1 -1
- package/dist/utils/spec-files.js +26 -51
- package/dist/utils/spec-files.js.map +1 -1
- package/dist/utils/task-progress.d.ts +11 -9
- package/dist/utils/task-progress.d.ts.map +1 -1
- package/dist/utils/task-progress.js +53 -32
- package/dist/utils/task-progress.js.map +1 -1
- package/dist/utils/timestamp.d.ts +5 -8
- package/dist/utils/timestamp.d.ts.map +1 -1
- package/dist/utils/timestamp.js +5 -8
- package/dist/utils/timestamp.js.map +1 -1
- package/package.json +2 -3
- package/schemas/decision/templates/decision.md +3 -1
- package/schemas/decision/templates/index.md +2 -2
- package/schemas/issue/schema.yaml +8 -1
- package/schemas/issue/templates/spec.md +27 -2
- package/schemas/sdd/schema.yaml +20 -1
- package/schemas/sdd/templates/spec.md +27 -2
- /package/assets/rules/tospec/{single-sourc-of-truth.md → single-source-of-truth.md} +0 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { promises as fs } from 'fs';
|
|
3
3
|
import { Validator } from '../core/validation/validator.js';
|
|
4
|
+
import { issueSymbol } from '../core/validation/types.js';
|
|
4
5
|
import { validateSections } from '../core/validation/section-validator.js';
|
|
5
6
|
import { resolveSchema, getSchemaDir } from '../core/artifact-graph/resolver.js';
|
|
6
7
|
import { resolveArtifactOutputs } from '../core/artifact-graph/outputs.js';
|
|
8
|
+
import { isStubArtifactFile } from '../core/artifact-graph/stub-detection.js';
|
|
7
9
|
import { METADATA_FILENAME, readSkipSpecsMarker, resolveSchemaForChange, } from '../utils/change-metadata.js';
|
|
8
10
|
import { FileSystemUtils } from '../utils/file-system.js';
|
|
9
11
|
import { findSpecFiles } from '../utils/spec-files.js';
|
|
@@ -15,44 +17,52 @@ import { getAvailableChanges } from './workflow/shared.js';
|
|
|
15
17
|
import { nearestMatches } from '../utils/match.js';
|
|
16
18
|
import { resolveTaskFiles } from '../utils/task-progress.js';
|
|
17
19
|
import { findTaskNumberingIssues, } from '../core/validation/task-numbering.js';
|
|
20
|
+
/**
|
|
21
|
+
* The one null-shape every `validate --json` failure carries, so a caller
|
|
22
|
+
* reading `result.summary.totals` never gets `undefined` from one failure path
|
|
23
|
+
* and `null` from another.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* The `--json` null-shape, `root` included: a root-resolution failure has none,
|
|
27
|
+
* and the sites that do resolve one spread their own over it.
|
|
28
|
+
*/
|
|
29
|
+
export const VALIDATE_FAILURE_PAYLOAD = { items: null, summary: null, root: null };
|
|
18
30
|
export class ValidateCommand {
|
|
19
31
|
async execute(itemName, options = {}) {
|
|
20
|
-
// A bulk run is the one shape that can succeed vacuously:
|
|
21
|
-
// root
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// root there. Named-item validation keeps it: that path fails loudly on its
|
|
25
|
-
// own when the item cannot be found, so it cannot pass vacuously.
|
|
32
|
+
// A bulk run is the one shape that can succeed vacuously: on an implicit
|
|
33
|
+
// root that is not a project it would report "0 items, 0 failed" with exit 0
|
|
34
|
+
// about a place it never checked. Named-item validation keeps the implicit
|
|
35
|
+
// root; it fails loudly instead.
|
|
26
36
|
const isBulk = !!(options.all || options.changes || options.specs);
|
|
27
37
|
const root = await resolveRootForCommand(options, {
|
|
28
38
|
json: options.json,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
: {}),
|
|
39
|
+
failurePayload: VALIDATE_FAILURE_PAYLOAD,
|
|
40
|
+
...(isBulk ? { allowImplicitRoot: false } : {}),
|
|
32
41
|
});
|
|
33
42
|
if (!root) {
|
|
34
43
|
return;
|
|
35
44
|
}
|
|
36
45
|
const interactive = isInteractive(options);
|
|
37
|
-
// Handle bulk flags first
|
|
38
46
|
if (isBulk) {
|
|
47
|
+
// Stderr, as `show` and `list` do for a flag their mode ignores: silently
|
|
48
|
+
// validating everything reads as a verdict on the one item named.
|
|
49
|
+
if (itemName) {
|
|
50
|
+
console.error(`Warning: Ignoring item '${itemName}'; --all/--changes/--specs validate every item.`);
|
|
51
|
+
}
|
|
39
52
|
await this.runBulkValidation(root, {
|
|
40
53
|
changes: !!options.all || !!options.changes,
|
|
41
54
|
specs: !!options.all || !!options.specs,
|
|
42
55
|
}, { strict: !!options.strict, json: !!options.json, concurrency: options.concurrency, noInteractive: resolveNoInteractive(options) });
|
|
43
56
|
return;
|
|
44
57
|
}
|
|
45
|
-
// No item and no flags
|
|
46
58
|
if (!itemName) {
|
|
47
59
|
if (interactive) {
|
|
48
60
|
await this.runInteractiveSelector(root, { strict: !!options.strict, json: !!options.json, concurrency: options.concurrency });
|
|
49
61
|
return;
|
|
50
62
|
}
|
|
51
|
-
this.
|
|
52
|
-
process.exitCode = 1;
|
|
63
|
+
this.reportNothingToValidate(root, !!options.json);
|
|
53
64
|
return;
|
|
54
65
|
}
|
|
55
|
-
// Direct item validation with type detection or override
|
|
56
66
|
const typeOverride = this.normalizeType(options.type);
|
|
57
67
|
await this.validateDirectItem(root, itemName, { typeOverride, strict: !!options.strict, json: !!options.json });
|
|
58
68
|
}
|
|
@@ -65,11 +75,9 @@ export class ValidateCommand {
|
|
|
65
75
|
return undefined;
|
|
66
76
|
}
|
|
67
77
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* scaffolded or still-authoring change that the sibling commands already
|
|
72
|
-
* resolve (#1182). Sorted to preserve the prior `getActiveChangeIds` ordering.
|
|
78
|
+
* Change IDs by directory existence — the same rule `status` and
|
|
79
|
+
* `instructions` use — rather than by `proposal.md`, so a scaffolded change
|
|
80
|
+
* resolves here too.
|
|
73
81
|
*/
|
|
74
82
|
async listChangeIds(root) {
|
|
75
83
|
const ids = await getAvailableChanges(root.path, root.changesDir);
|
|
@@ -92,7 +100,6 @@ export class ValidateCommand {
|
|
|
92
100
|
return this.runBulkValidation(root, { changes: true, specs: false }, opts);
|
|
93
101
|
if (choice === 'specs')
|
|
94
102
|
return this.runBulkValidation(root, { changes: false, specs: true }, opts);
|
|
95
|
-
// one
|
|
96
103
|
const [changes, specs] = await Promise.all([this.listChangeIds(root), getSpecIds(root.path)]);
|
|
97
104
|
const items = [];
|
|
98
105
|
items.push(...changes.map(id => ({ name: `change/${id}`, value: { type: 'change', id } })));
|
|
@@ -105,26 +112,70 @@ export class ValidateCommand {
|
|
|
105
112
|
const picked = await select({ message: 'Pick an item', choices: items });
|
|
106
113
|
await this.validateByType(root, picked.type, picked.id, opts);
|
|
107
114
|
}
|
|
108
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Nothing named and no terminal to ask in. `no_item_specified` is the code
|
|
117
|
+
* `show` uses for the same situation.
|
|
118
|
+
*/
|
|
119
|
+
reportNothingToValidate(root, json) {
|
|
120
|
+
const fix = [
|
|
121
|
+
'tospec validate --all',
|
|
122
|
+
'tospec validate --changes',
|
|
123
|
+
'tospec validate --specs',
|
|
124
|
+
'tospec validate <item-name>',
|
|
125
|
+
];
|
|
126
|
+
if (json) {
|
|
127
|
+
emitFailureStatus({ ...VALIDATE_FAILURE_PAYLOAD, root: toRootOutput(root) }, {
|
|
128
|
+
severity: 'error',
|
|
129
|
+
code: 'no_item_specified',
|
|
130
|
+
message: 'Nothing to validate.',
|
|
131
|
+
fix: fix.join('\n'),
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
109
135
|
console.error('Nothing to validate. Try one of:');
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
console.error(` ${'tospec validate --specs'}`);
|
|
113
|
-
console.error(` ${'tospec validate <item-name>'}`);
|
|
136
|
+
for (const line of fix)
|
|
137
|
+
console.error(` ${line}`);
|
|
114
138
|
console.error('Or run in an interactive terminal.');
|
|
139
|
+
process.exitCode = 1;
|
|
115
140
|
}
|
|
116
141
|
async validateDirectItem(root, itemName, opts) {
|
|
117
142
|
const [changes, specs] = await Promise.all([this.listChangeIds(root), getSpecIds(root.path)]);
|
|
118
143
|
const isChange = changes.includes(itemName);
|
|
119
144
|
const isSpec = specs.includes(itemName);
|
|
120
|
-
|
|
145
|
+
let type = opts.typeOverride;
|
|
121
146
|
if (!type) {
|
|
122
|
-
|
|
147
|
+
if (isChange)
|
|
148
|
+
type = 'change';
|
|
149
|
+
else if (isSpec)
|
|
150
|
+
type = 'spec';
|
|
151
|
+
}
|
|
152
|
+
// An explicit --type names a kind the item is not: say that, rather than
|
|
153
|
+
// letting the read fail with the ENOENT of a path the caller never typed.
|
|
154
|
+
if (opts.typeOverride && !(opts.typeOverride === 'change' ? isChange : isSpec)) {
|
|
155
|
+
const location = opts.typeOverride === 'change'
|
|
156
|
+
? path.join(root.changesDir, itemName)
|
|
157
|
+
: path.join(root.specsDir, itemName, 'spec.md');
|
|
158
|
+
const otherKind = opts.typeOverride === 'change' ? (isSpec ? 'spec' : undefined) : isChange ? 'change' : undefined;
|
|
159
|
+
const message = `${opts.typeOverride === 'change' ? 'Change' : 'Spec'} '${itemName}' not found at ${location}.` +
|
|
160
|
+
(otherKind ? ` A ${otherKind} with that name exists: pass --type ${otherKind}.` : '');
|
|
161
|
+
if (opts.json) {
|
|
162
|
+
emitFailureStatus({ ...VALIDATE_FAILURE_PAYLOAD, root: toRootOutput(root) }, { severity: 'error', code: 'unknown_item', message });
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
console.error(message);
|
|
166
|
+
process.exitCode = 1;
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (!type) {
|
|
171
|
+
// Deduplicated: a name that is both a change and a spec would otherwise
|
|
172
|
+
// be suggested twice.
|
|
173
|
+
const suggestions = nearestMatches(itemName, [...new Set([...changes, ...specs])]);
|
|
123
174
|
const message = suggestions.length
|
|
124
175
|
? `Unknown item '${itemName}'. Did you mean: ${suggestions.join(', ')}?`
|
|
125
176
|
: `Unknown item '${itemName}'.`;
|
|
126
177
|
if (opts.json) {
|
|
127
|
-
emitFailureStatus({
|
|
178
|
+
emitFailureStatus({ ...VALIDATE_FAILURE_PAYLOAD, root: toRootOutput(root) }, { severity: 'error', code: 'unknown_item', message });
|
|
128
179
|
}
|
|
129
180
|
else {
|
|
130
181
|
console.error(message);
|
|
@@ -134,7 +185,7 @@ export class ValidateCommand {
|
|
|
134
185
|
}
|
|
135
186
|
if (!opts.typeOverride && isChange && isSpec) {
|
|
136
187
|
if (opts.json) {
|
|
137
|
-
emitFailureStatus({
|
|
188
|
+
emitFailureStatus({ ...VALIDATE_FAILURE_PAYLOAD, root: toRootOutput(root) }, {
|
|
138
189
|
severity: 'error',
|
|
139
190
|
code: 'ambiguous_item',
|
|
140
191
|
message: `Ambiguous item '${itemName}' matches both a change and a spec.`,
|
|
@@ -156,8 +207,7 @@ export class ValidateCommand {
|
|
|
156
207
|
const start = Date.now();
|
|
157
208
|
const report = await validateChangeArtifacts(root.path, changeDir, { strict: opts.strict });
|
|
158
209
|
const durationMs = Date.now() - start;
|
|
159
|
-
this.printReport('change', id, report, durationMs, opts.json, root);
|
|
160
|
-
// Non-zero exit if invalid (keeps enriched output test semantics)
|
|
210
|
+
this.printReport('change', id, report, durationMs, opts.json, root, opts.strict);
|
|
161
211
|
process.exitCode = report.valid ? 0 : 1;
|
|
162
212
|
return;
|
|
163
213
|
}
|
|
@@ -165,13 +215,22 @@ export class ValidateCommand {
|
|
|
165
215
|
const start = Date.now();
|
|
166
216
|
const report = await validator.validateSpec(file);
|
|
167
217
|
const durationMs = Date.now() - start;
|
|
168
|
-
this.printReport('spec', id, report, durationMs, opts.json, root);
|
|
218
|
+
this.printReport('spec', id, report, durationMs, opts.json, root, opts.strict);
|
|
169
219
|
process.exitCode = report.valid ? 0 : 1;
|
|
170
220
|
}
|
|
171
|
-
printReport(type, id, report, durationMs, json, root) {
|
|
221
|
+
printReport(type, id, report, durationMs, json, root, strict = false) {
|
|
172
222
|
if (json) {
|
|
173
223
|
emitSuccess({
|
|
174
|
-
items: [
|
|
224
|
+
items: [
|
|
225
|
+
{
|
|
226
|
+
id,
|
|
227
|
+
type,
|
|
228
|
+
valid: report.valid,
|
|
229
|
+
issues: report.issues,
|
|
230
|
+
...(report.notes?.length ? { notes: report.notes } : {}),
|
|
231
|
+
durationMs,
|
|
232
|
+
},
|
|
233
|
+
],
|
|
175
234
|
summary: { totals: { items: 1, passed: report.valid ? 1 : 0, failed: report.valid ? 0 : 1 }, byType: { [type]: { items: 1, passed: report.valid ? 1 : 0, failed: report.valid ? 0 : 1 } } },
|
|
176
235
|
}, toRootOutput(root));
|
|
177
236
|
return;
|
|
@@ -182,51 +241,65 @@ export class ValidateCommand {
|
|
|
182
241
|
else {
|
|
183
242
|
console.error(`${type === 'change' ? 'Change' : 'Specification'} '${id}' has issues`);
|
|
184
243
|
}
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
// Printing only on failure made every one of them invisible outside --json,
|
|
189
|
-
// which implements the downgrade as a deletion.
|
|
190
|
-
//
|
|
191
|
-
// Routed with the verdict, though: on stderr they make a passing run look
|
|
192
|
-
// failed to anything that reads the stream as an error channel — a hook, a
|
|
193
|
-
// CI step, an agent — which is the same deletion by a different route, this
|
|
194
|
-
// time with a false alarm attached. Exit 0 leaves stderr empty.
|
|
244
|
+
// Printed whatever the verdict: several checks are WARNINGs so a run keeps
|
|
245
|
+
// passing while the author still learns about them. Routed *with* the
|
|
246
|
+
// verdict, so a passing run leaves stderr empty for hooks and CI.
|
|
195
247
|
const writeIssue = report.valid ? console.log : console.error;
|
|
196
248
|
for (const issue of report.issues) {
|
|
197
249
|
writeIssue(formatIssueLine(issue));
|
|
198
250
|
}
|
|
199
|
-
//
|
|
251
|
+
// Once each: a note explains the rule, which is the same however many lines
|
|
252
|
+
// tripped it.
|
|
253
|
+
for (const note of report.notes ?? []) {
|
|
254
|
+
writeIssue(`Note: ${note}`);
|
|
255
|
+
}
|
|
256
|
+
// Failure-only: a passing item has no next step to take.
|
|
200
257
|
if (!report.valid) {
|
|
201
|
-
this.printNextSteps(type, id, root, report.issues);
|
|
258
|
+
this.printNextSteps(type, id, root, report.issues, strict);
|
|
202
259
|
}
|
|
203
260
|
}
|
|
204
261
|
/**
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
* its delta headers instead.
|
|
262
|
+
* Hints derived from what actually failed, not from the item type, so a
|
|
263
|
+
* change whose only problem is a missing design.md section is not told to
|
|
264
|
+
* check delta headers.
|
|
209
265
|
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
* - a misnamed file in a capability folder (`auth/extra.md`) and a delta at
|
|
216
|
-
* the specs/ root (`spec.md`) are placement errors; each message already
|
|
217
|
-
* names the exact path to rename to, so the generic bullets would be
|
|
218
|
-
* advice for a different failure;
|
|
219
|
-
* - the whole-change `file` sentinel ("No deltas found") already embeds
|
|
220
|
-
* these same three tips via `enrichTopLevelError`.
|
|
266
|
+
* The delta hints are about delta *content*, so they match only an issue
|
|
267
|
+
* reported under `<capability>/spec.md`. The leading slash excludes a misnamed
|
|
268
|
+
* file and a delta at the specs/ root — placement errors whose messages
|
|
269
|
+
* already name the path to rename to — and the whole-change `file` sentinel,
|
|
270
|
+
* which `enrichTopLevelError` already covers.
|
|
221
271
|
*/
|
|
222
|
-
printNextSteps(type, id, root, issues) {
|
|
272
|
+
printNextSteps(type, id, root, issues, strict) {
|
|
223
273
|
const bullets = [];
|
|
224
274
|
if (type === 'change') {
|
|
225
|
-
|
|
226
|
-
|
|
275
|
+
// Each delta hint answers one failure. A missing Migration field or a
|
|
276
|
+
// SHALL/MUST warning under --strict is a delta issue too, and used to be
|
|
277
|
+
// told to add a Scenario block it already had.
|
|
278
|
+
const deltaIssues = issues.filter((issue) => issue.path.endsWith('/spec.md'));
|
|
279
|
+
const mentions = (pattern) => deltaIssues.some((issue) => pattern.test(issue.message));
|
|
280
|
+
if (mentions(/delta header|No deltas found|not a delta|## ADDED/i)) {
|
|
227
281
|
bullets.push('- Ensure change has deltas in specs/: use headers ## ADDED/MODIFIED/REMOVED/RENAMED Requirements');
|
|
282
|
+
}
|
|
283
|
+
if (mentions(/#### Scenario:/)) {
|
|
228
284
|
bullets.push('- Each requirement MUST include at least one #### Scenario: block');
|
|
229
|
-
|
|
285
|
+
}
|
|
286
|
+
if (mentions(/omits scenario/)) {
|
|
287
|
+
bullets.push('- Restate every scenario the current spec still has in the MODIFIED block, or declare the removal under ## REMOVED Scenarios (- Requirement: `<name>` / - Scenario: `<name>` / - Reason: ...)');
|
|
288
|
+
}
|
|
289
|
+
if (mentions(/Migration field/)) {
|
|
290
|
+
bullets.push('- Add a **Migration** field to each REMOVED requirement');
|
|
291
|
+
}
|
|
292
|
+
if (mentions(/SHALL or MUST/)) {
|
|
293
|
+
// The keyword check is English-only, and the way out for a non-English
|
|
294
|
+
// spec is to drop --strict — advice that only makes sense when it is on.
|
|
295
|
+
bullets.push(strict
|
|
296
|
+
? '- State the obligation with SHALL or MUST in the requirement body (or run without --strict for non-English specs)'
|
|
297
|
+
: '- State the obligation with SHALL or MUST in the requirement body');
|
|
298
|
+
}
|
|
299
|
+
// Only a delta issue that failed the run: a WARNING on an otherwise valid
|
|
300
|
+
// delta (a missing Purpose, say) is not something `show --json` explains.
|
|
301
|
+
if (deltaIssues.some((issue) => issue.level === 'ERROR' || strict)) {
|
|
302
|
+
bullets.push(`- Debug parsed deltas: ${`tospec show ${id} --json`}`);
|
|
230
303
|
}
|
|
231
304
|
const hasMissingSection = issues.some((issue) => issue.message.startsWith('Missing required section'));
|
|
232
305
|
if (hasMissingSection) {
|
|
@@ -238,8 +311,7 @@ export class ValidateCommand {
|
|
|
238
311
|
bullets.push('- Each requirement MUST include at least one #### Scenario: block');
|
|
239
312
|
bullets.push('- Re-run with --json to see structured report');
|
|
240
313
|
}
|
|
241
|
-
//
|
|
242
|
-
// be advice for a different failure, so print nothing instead.
|
|
314
|
+
// Nothing applies: generic bullets would be advice for a different failure.
|
|
243
315
|
if (bullets.length === 0)
|
|
244
316
|
return;
|
|
245
317
|
console.error('Next steps:');
|
|
@@ -255,7 +327,7 @@ export class ValidateCommand {
|
|
|
255
327
|
const concurrency = normalizeConcurrency(opts.concurrency) ?? normalizeConcurrency(process.env.TOSPEC_CONCURRENCY) ?? DEFAULT_CONCURRENCY;
|
|
256
328
|
const validator = new Validator(opts.strict);
|
|
257
329
|
// Each entry carries its own id/type so a rejected task can be reported
|
|
258
|
-
// without re-deriving them from its
|
|
330
|
+
// without re-deriving them from its position.
|
|
259
331
|
const queue = [];
|
|
260
332
|
for (const id of changeIds) {
|
|
261
333
|
queue.push({
|
|
@@ -266,7 +338,7 @@ export class ValidateCommand {
|
|
|
266
338
|
const changeDir = path.join(root.changesDir, id);
|
|
267
339
|
const report = await validateChangeArtifacts(root.path, changeDir, { strict: opts.strict });
|
|
268
340
|
const durationMs = Date.now() - start;
|
|
269
|
-
return { id, type: 'change', valid: report.valid, issues: report.issues, durationMs };
|
|
341
|
+
return { id, type: 'change', valid: report.valid, issues: report.issues, notes: report.notes, durationMs };
|
|
270
342
|
},
|
|
271
343
|
});
|
|
272
344
|
}
|
|
@@ -279,7 +351,7 @@ export class ValidateCommand {
|
|
|
279
351
|
const file = path.join(root.specsDir, id, 'spec.md');
|
|
280
352
|
const report = await validator.validateSpec(file);
|
|
281
353
|
const durationMs = Date.now() - start;
|
|
282
|
-
return { id, type: 'spec', valid: report.valid, issues: report.issues, durationMs };
|
|
354
|
+
return { id, type: 'spec', valid: report.valid, issues: report.issues, notes: report.notes, durationMs };
|
|
283
355
|
},
|
|
284
356
|
});
|
|
285
357
|
}
|
|
@@ -338,8 +410,8 @@ export class ValidateCommand {
|
|
|
338
410
|
next();
|
|
339
411
|
});
|
|
340
412
|
results.sort((a, b) => a.id.localeCompare(b.id));
|
|
341
|
-
// Derived from results rather than counted alongside them
|
|
342
|
-
//
|
|
413
|
+
// Derived from results rather than counted alongside them, so the two
|
|
414
|
+
// cannot disagree.
|
|
343
415
|
const passed = results.filter((res) => res.valid).length;
|
|
344
416
|
const failed = results.length - passed;
|
|
345
417
|
const summary = {
|
|
@@ -349,8 +421,11 @@ export class ValidateCommand {
|
|
|
349
421
|
...(scope.specs ? { spec: summarizeType(results, 'spec') } : {}),
|
|
350
422
|
},
|
|
351
423
|
};
|
|
424
|
+
// One copy per run: a sweep of forty specs trips the same rule forty times.
|
|
425
|
+
const runNotes = [...new Set(results.flatMap((res) => res.notes ?? []))];
|
|
426
|
+
const items = results.map(({ notes: _notes, ...res }) => res);
|
|
352
427
|
if (opts.json) {
|
|
353
|
-
emitSuccess({ items
|
|
428
|
+
emitSuccess({ items, summary, ...(runNotes.length ? { notes: runNotes } : {}) }, toRootOutput(root));
|
|
354
429
|
}
|
|
355
430
|
else {
|
|
356
431
|
for (const res of results) {
|
|
@@ -358,31 +433,35 @@ export class ValidateCommand {
|
|
|
358
433
|
console.log(`✓ ${res.type}/${res.id}`);
|
|
359
434
|
else
|
|
360
435
|
console.error(`✗ ${res.type}/${res.id}`);
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
// and the `Details:` hint below only ever names a failure. Each item's
|
|
364
|
-
// findings follow its own verdict onto the same stream, so a run where
|
|
365
|
-
// everything passes writes nothing to stderr.
|
|
436
|
+
// Routed with the item's own verdict, so a run where everything passes
|
|
437
|
+
// writes nothing to stderr.
|
|
366
438
|
const writeIssue = res.valid ? console.log : console.error;
|
|
367
439
|
for (const issue of res.issues) {
|
|
368
440
|
writeIssue(formatIssueLine(issue, ' '));
|
|
369
441
|
}
|
|
370
442
|
}
|
|
443
|
+
for (const note of runNotes) {
|
|
444
|
+
console.log(`Note: ${note}`);
|
|
445
|
+
}
|
|
371
446
|
console.log(`Totals: ${summary.totals.passed} passed, ${summary.totals.failed} failed (${summary.totals.items} items)`);
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
447
|
+
// One line per failed item: naming only the first left the rest without
|
|
448
|
+
// a command to rerun.
|
|
449
|
+
const failures = results.filter((res) => !res.valid);
|
|
450
|
+
if (failures.length > 0) {
|
|
451
|
+
console.log('Details:');
|
|
452
|
+
for (const failure of failures) {
|
|
453
|
+
console.log(` tospec validate ${failure.id} --type ${failure.type}`);
|
|
454
|
+
}
|
|
375
455
|
}
|
|
376
456
|
}
|
|
377
457
|
process.exitCode = failed > 0 ? 1 : 0;
|
|
378
458
|
}
|
|
379
459
|
}
|
|
380
460
|
/**
|
|
381
|
-
* Validates schema-declared `requiredSections` for
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
* not validate's.
|
|
461
|
+
* Validates schema-declared `requiredSections` for artifacts with no Requirement
|
|
462
|
+
* grammar of their own. Only artifacts that declare a `validation` block and
|
|
463
|
+
* already have an output file are checked — one not yet produced is `status`'s
|
|
464
|
+
* concern, not validate's.
|
|
386
465
|
*/
|
|
387
466
|
export async function validateArtifactSections(projectRoot, changeDir) {
|
|
388
467
|
const issues = [];
|
|
@@ -397,17 +476,27 @@ export async function validateArtifactSections(projectRoot, changeDir) {
|
|
|
397
476
|
}
|
|
398
477
|
const schemaDir = getSchemaDir(schemaName, projectRoot);
|
|
399
478
|
for (const artifact of schema.artifacts) {
|
|
400
|
-
if (!artifact.validation)
|
|
401
|
-
continue;
|
|
402
479
|
for (const file of resolveArtifactOutputs(changeDir, artifact.generates)) {
|
|
403
480
|
const relPath = FileSystemUtils.toPosixPath(path.relative(changeDir, file));
|
|
481
|
+
// Runs before the section rules because a template satisfies them by
|
|
482
|
+
// construction — it is where every required heading came from. WARNING, so
|
|
483
|
+
// `--strict` is what refuses it.
|
|
484
|
+
if (isStubArtifactFile(file, schemaName, artifact.template, projectRoot)) {
|
|
485
|
+
issues.push({
|
|
486
|
+
level: 'WARNING',
|
|
487
|
+
path: relPath,
|
|
488
|
+
message: `Artifact "${artifact.id}" is still the unfilled template. Run tospec instructions ${artifact.id} --change <name> --json, then replace the file's contents.`,
|
|
489
|
+
});
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (!artifact.validation)
|
|
493
|
+
continue;
|
|
404
494
|
let content;
|
|
405
495
|
try {
|
|
406
496
|
content = await fs.readFile(file, 'utf-8');
|
|
407
497
|
}
|
|
408
498
|
catch (error) {
|
|
409
|
-
//
|
|
410
|
-
// per-file error instead of crashing the whole command.
|
|
499
|
+
// Vanished between discovery and read: a per-file error, not a crash.
|
|
411
500
|
issues.push({
|
|
412
501
|
level: 'ERROR',
|
|
413
502
|
path: relPath,
|
|
@@ -416,7 +505,7 @@ export async function validateArtifactSections(projectRoot, changeDir) {
|
|
|
416
505
|
continue;
|
|
417
506
|
}
|
|
418
507
|
const templateRef = schemaDir
|
|
419
|
-
?
|
|
508
|
+
? describeTemplatePath(projectRoot, schemaDir, artifact.template)
|
|
420
509
|
: undefined;
|
|
421
510
|
issues.push(...validateSections(content, artifact.validation, { path: relPath, templateRef }));
|
|
422
511
|
}
|
|
@@ -426,9 +515,9 @@ export async function validateArtifactSections(projectRoot, changeDir) {
|
|
|
426
515
|
const EMPTY_REPORT = { valid: true, issues: [], summary: { errors: 0, warnings: 0, info: 0 } };
|
|
427
516
|
/**
|
|
428
517
|
* Ambiguous task numbering, as WARNINGs. Task IDs are how apply refers to work,
|
|
429
|
-
* so a duplicate
|
|
430
|
-
*
|
|
431
|
-
*
|
|
518
|
+
* so a duplicate makes that reference point at the wrong item — but the
|
|
519
|
+
* numbering is a convention, not a structural requirement, so it reports rather
|
|
520
|
+
* than blocks (`--strict` still refuses).
|
|
432
521
|
*/
|
|
433
522
|
async function validateTaskNumbering(projectRoot, changeDir) {
|
|
434
523
|
const documents = [];
|
|
@@ -451,11 +540,8 @@ async function validateTaskNumbering(projectRoot, changeDir) {
|
|
|
451
540
|
}));
|
|
452
541
|
}
|
|
453
542
|
/**
|
|
454
|
-
* The single validation pipeline for a change
|
|
455
|
-
* `
|
|
456
|
-
* can never disagree ("validate green but archive blocked" is structurally
|
|
457
|
-
* impossible). Runs delta-spec validation (when the schema requires deltas
|
|
458
|
-
* or the change produced any) plus schema-declared section validation.
|
|
543
|
+
* The single validation pipeline for a change, used verbatim by `validate` and
|
|
544
|
+
* `archive`, so "validate green but archive blocked" is structurally impossible.
|
|
459
545
|
*/
|
|
460
546
|
export async function validateChangeArtifacts(projectRoot, changeDir, options = {}) {
|
|
461
547
|
const strict = options.strict ?? false;
|
|
@@ -463,11 +549,24 @@ export async function validateChangeArtifacts(projectRoot, changeDir, options =
|
|
|
463
549
|
const validator = new Validator(strict);
|
|
464
550
|
const marker = readSkipSpecsMarker(changeDir);
|
|
465
551
|
const markerIssues = await skipSpecsMarkerIssues(changeDir, marker);
|
|
466
|
-
// A
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
|
|
552
|
+
// A schema that cannot be resolved is the one finding: every other check
|
|
553
|
+
// would run against `sdd` by default and report "no delta" about a change
|
|
554
|
+
// whose real problem is the schema name. Unparseable metadata is already
|
|
555
|
+
// reported by the marker, and the schema cannot be read out of it either.
|
|
556
|
+
if (!marker.unreadableReason) {
|
|
557
|
+
const schemaIssue = schemaResolutionIssue(projectRoot, changeDir);
|
|
558
|
+
if (schemaIssue) {
|
|
559
|
+
return mergeSectionIssues(EMPTY_REPORT, [schemaIssue, ...markerIssues], strict);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
// A declared marker waives the delta requirement; a *contradicted* one
|
|
563
|
+
// (declared, yet delta files exist) must not, because those files are real.
|
|
564
|
+
//
|
|
565
|
+
// Unparseable metadata waives it for the opposite reason: whether skip_specs
|
|
566
|
+
// was declared is unknowable, so "add a delta or set skip_specs" dead-ends a
|
|
567
|
+
// user who already set it inside the malformed file.
|
|
568
|
+
const requireDeltas = !marker.unreadableReason &&
|
|
569
|
+
(!marker.declared || markerIssues.length > 0) &&
|
|
471
570
|
(await deltaSpecsRequired(projectRoot, changeDir));
|
|
472
571
|
const [deltaReport, sectionIssues, numberingIssues] = await Promise.all([
|
|
473
572
|
requireDeltas
|
|
@@ -481,12 +580,43 @@ export async function validateChangeArtifacts(projectRoot, changeDir, options =
|
|
|
481
580
|
return mergeSectionIssues(deltaReport, [...sectionIssues, ...numberingIssues, ...markerIssues], strict);
|
|
482
581
|
}
|
|
483
582
|
/**
|
|
484
|
-
*
|
|
485
|
-
*
|
|
486
|
-
*
|
|
487
|
-
*
|
|
583
|
+
* The ERROR for a change whose schema cannot be resolved (`.tospec.yaml` names
|
|
584
|
+
* one that does not exist, or the schema file itself is broken), or null.
|
|
585
|
+
* Same message `list` and `status` give, so the three commands agree on what
|
|
586
|
+
* is wrong with the change.
|
|
587
|
+
*/
|
|
588
|
+
function schemaResolutionIssue(projectRoot, changeDir) {
|
|
589
|
+
try {
|
|
590
|
+
resolveSchema(resolveSchemaForChange(changeDir, undefined, projectRoot), projectRoot);
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
catch (error) {
|
|
594
|
+
const reason = (error instanceof Error ? error.message : String(error)).replace(/\.?\s*$/, '.');
|
|
595
|
+
return {
|
|
596
|
+
level: 'ERROR',
|
|
597
|
+
path: METADATA_FILENAME,
|
|
598
|
+
message: `${reason} Fix the schema name in ${METADATA_FILENAME} before anything else in this change can be checked.`,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Errors in the skip_specs declaration itself: unparseable metadata, an unusable
|
|
604
|
+
* value, or a change claiming no specs while carrying delta files. Each is a
|
|
605
|
+
* contradiction the user has to resolve.
|
|
606
|
+
*
|
|
607
|
+
* Most-fundamental first and one at a time: a file that does not parse cannot
|
|
608
|
+
* also be said to hold a wrong value.
|
|
488
609
|
*/
|
|
489
610
|
async function skipSpecsMarkerIssues(changeDir, marker) {
|
|
611
|
+
if (marker.unreadableReason) {
|
|
612
|
+
return [
|
|
613
|
+
{
|
|
614
|
+
level: 'ERROR',
|
|
615
|
+
path: METADATA_FILENAME,
|
|
616
|
+
message: `Invalid YAML in ${METADATA_FILENAME}: ${marker.unreadableReason} Nothing in this file can be read until it parses — including \`skip_specs\`, so no other check about it means anything yet.`,
|
|
617
|
+
},
|
|
618
|
+
];
|
|
619
|
+
}
|
|
490
620
|
if (marker.invalidReason) {
|
|
491
621
|
return [
|
|
492
622
|
{
|
|
@@ -515,13 +645,9 @@ async function skipSpecsMarkerIssues(changeDir, marker) {
|
|
|
515
645
|
}
|
|
516
646
|
/**
|
|
517
647
|
* Whether a change's schema requires at least one delta spec — a property of the
|
|
518
|
-
* schema, not of the change.
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
* `optional` and this change produced none. A pure fix with no behavioral change
|
|
522
|
-
* (e.g. an issue schema change) is a legitimate, complete change, not a
|
|
523
|
-
* validation failure. Falls back to requiring deltas (the stricter, pre-existing
|
|
524
|
-
* behavior) when the schema can't be resolved.
|
|
648
|
+
* schema, not of the change. False when the schema declares no `specs` artifact,
|
|
649
|
+
* or declares it `optional` and this change produced none: a pure fix with no
|
|
650
|
+
* behavioral change is a complete change, not a validation failure.
|
|
525
651
|
*/
|
|
526
652
|
export async function deltaSpecsRequired(projectRoot, changeDir) {
|
|
527
653
|
let schemaName;
|
|
@@ -531,16 +657,14 @@ export async function deltaSpecsRequired(projectRoot, changeDir) {
|
|
|
531
657
|
schema = resolveSchema(schemaName, projectRoot);
|
|
532
658
|
}
|
|
533
659
|
catch {
|
|
534
|
-
// Nothing is known about what this schema
|
|
535
|
-
// failing loudly beats passing a change nobody examined.
|
|
536
|
-
//
|
|
660
|
+
// Nothing is known about what this schema produces, so demand the delta:
|
|
661
|
+
// failing loudly beats passing a change nobody examined. The opposite
|
|
662
|
+
// answer from the `!specsArtifact` branch below, deliberately.
|
|
537
663
|
return true;
|
|
538
664
|
}
|
|
539
665
|
const specsArtifact = schema.artifacts.find((a) => a.id === 'specs');
|
|
540
|
-
//
|
|
541
|
-
//
|
|
542
|
-
// schema, so it never appears in `status` and no instruction ever writes it —
|
|
543
|
-
// and an unsatisfiable error is a dead end, not a rule.
|
|
666
|
+
// Deltas are not on this schema's list, so asking for one is unsatisfiable:
|
|
667
|
+
// the file never appears in `status` and no instruction ever writes it.
|
|
544
668
|
if (!specsArtifact)
|
|
545
669
|
return false;
|
|
546
670
|
if (!specsArtifact.optional)
|
|
@@ -558,15 +682,24 @@ export function mergeSectionIssues(report, extra, strict) {
|
|
|
558
682
|
return { valid, issues, summary: { errors, warnings, info } };
|
|
559
683
|
}
|
|
560
684
|
/**
|
|
561
|
-
*
|
|
562
|
-
*
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
*
|
|
566
|
-
|
|
685
|
+
* Where to find an artifact's template, in whichever form is openable: relative
|
|
686
|
+
* to the project root when the schema lives inside it, absolute otherwise.
|
|
687
|
+
* `path.relative` on a packaged schema produces routes *out* of the project that
|
|
688
|
+
* say nothing about what they are relative to. Absolute also matches what
|
|
689
|
+
* `tospec templates --json` answers.
|
|
690
|
+
*/
|
|
691
|
+
function describeTemplatePath(projectRoot, schemaDir, template) {
|
|
692
|
+
const absolute = path.join(schemaDir, 'templates', template);
|
|
693
|
+
const relative = path.relative(projectRoot, absolute);
|
|
694
|
+
const insideProject = relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative);
|
|
695
|
+
return FileSystemUtils.toPosixPath(insideProject ? relative : absolute);
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* One rendering of a finding for every human surface. The named-item and bulk
|
|
699
|
+
* paths print the same three fields and differ only by indentation.
|
|
567
700
|
*/
|
|
568
701
|
export function formatIssueLine(issue, indent = '') {
|
|
569
|
-
const prefix = issue.level
|
|
702
|
+
const prefix = issueSymbol(issue.level);
|
|
570
703
|
return `${indent}${prefix} [${issue.level}] ${issue.path}: ${issue.message}`;
|
|
571
704
|
}
|
|
572
705
|
function summarizeType(results, type) {
|