@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
package/dist/core/specs-apply.js
CHANGED
|
@@ -1,22 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Spec Application Logic
|
|
3
|
-
*
|
|
4
|
-
* Extracted from ArchiveCommand to enable standalone spec application.
|
|
5
|
-
* Applies delta specs from a change to main specs without archiving.
|
|
6
|
-
*/
|
|
1
|
+
/** Applies a change's delta specs to the main specs, independently of archiving. */
|
|
7
2
|
import { promises as fs } from 'fs';
|
|
8
3
|
import path from 'path';
|
|
9
|
-
import { extractRequirementsSection, parseDeltaSpec, normalizeRequirementName, foldRequirementName, } from './parsers/requirement-blocks.js';
|
|
4
|
+
import { extractRequirementsSection, parseDeltaSpec, normalizeRequirementName, foldRequirementName, REQUIREMENT_HEADER_REGEX, } from './parsers/requirement-blocks.js';
|
|
10
5
|
import { findMainSpecStructureIssues } from './parsers/spec-structure.js';
|
|
11
6
|
import { isAbsentDirectoryError, isMissingPathError } from '../utils/file-system.js';
|
|
12
|
-
import { buildCodeFenceMask, findMissingScenarios } from './parsers/requirement-text.js';
|
|
7
|
+
import { buildCodeFenceMask, findMissingScenarios, normalizeDocument } from './parsers/requirement-text.js';
|
|
13
8
|
import { PURPOSE_PLACEHOLDER_PREFIX, PURPOSE_PLACEHOLDER_SUFFIX, } from './validation/constants.js';
|
|
14
|
-
// -----------------------------------------------------------------------------
|
|
15
|
-
// Public API
|
|
16
|
-
// -----------------------------------------------------------------------------
|
|
17
|
-
/**
|
|
18
|
-
* Find all delta spec files that need to be applied from a change.
|
|
19
|
-
*/
|
|
20
9
|
export async function findSpecUpdates(changeDir, mainSpecsDir) {
|
|
21
10
|
const updates = [];
|
|
22
11
|
const changeSpecsDir = path.join(changeDir, 'specs');
|
|
@@ -25,9 +14,8 @@ export async function findSpecUpdates(changeDir, mainSpecsDir) {
|
|
|
25
14
|
entries = await fs.readdir(changeSpecsDir, { withFileTypes: true });
|
|
26
15
|
}
|
|
27
16
|
catch (error) {
|
|
28
|
-
// An empty result
|
|
29
|
-
// on
|
|
30
|
-
// genuinely not there may say that.
|
|
17
|
+
// An empty result means "no deltas", and archive merges nothing and reports
|
|
18
|
+
// success on it. Only a genuinely absent directory may say that.
|
|
31
19
|
if (isAbsentDirectoryError(error))
|
|
32
20
|
return updates;
|
|
33
21
|
throw error;
|
|
@@ -41,16 +29,14 @@ export async function findSpecUpdates(changeDir, mainSpecsDir) {
|
|
|
41
29
|
await fs.access(specFile);
|
|
42
30
|
}
|
|
43
31
|
catch (error) {
|
|
44
|
-
// A capability folder with no spec.md is not a delta
|
|
45
|
-
//
|
|
46
|
-
// capability's requirements without a word.
|
|
32
|
+
// A capability folder with no spec.md is not a delta; an unreachable
|
|
33
|
+
// spec.md is one, and skipping it drops its requirements silently.
|
|
47
34
|
if (isMissingPathError(error))
|
|
48
35
|
continue;
|
|
49
36
|
throw error;
|
|
50
37
|
}
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
// new-capability path — which rebuilds the spec from a skeleton.
|
|
38
|
+
// Same rule: calling an unreachable target absent sends the merge down the
|
|
39
|
+
// new-capability path, which rebuilds the spec from a skeleton.
|
|
54
40
|
let exists = true;
|
|
55
41
|
try {
|
|
56
42
|
await fs.access(targetFile);
|
|
@@ -69,40 +55,53 @@ export async function findSpecUpdates(changeDir, mainSpecsDir) {
|
|
|
69
55
|
return updates;
|
|
70
56
|
}
|
|
71
57
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
58
|
+
* The capability directory's real name on disk, or `fallback` when absent.
|
|
59
|
+
* Differs only on a case-insensitive filesystem, where `specs/RegexCap` and
|
|
60
|
+
* `specs/regexcap` are one directory — this normalises a message, it does not
|
|
61
|
+
* merge anything.
|
|
74
62
|
*/
|
|
63
|
+
async function resolveOnDiskSpecName(targetPath, fallback) {
|
|
64
|
+
const parent = path.dirname(path.dirname(targetPath));
|
|
65
|
+
try {
|
|
66
|
+
const entries = await fs.readdir(parent);
|
|
67
|
+
return entries.find((entry) => entry.toLowerCase() === fallback.toLowerCase()) ?? fallback;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return fallback;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
75
73
|
export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
76
|
-
|
|
74
|
+
const notices = [];
|
|
75
|
+
const notice = (code, message) => {
|
|
76
|
+
notices.push({ code, message });
|
|
77
|
+
if (!options.silent)
|
|
78
|
+
console.log(`Warning: ${message}`);
|
|
79
|
+
};
|
|
77
80
|
const changeContent = await fs.readFile(update.source, 'utf-8');
|
|
78
|
-
// Parse deltas from the change spec file
|
|
79
81
|
const plan = parseDeltaSpec(changeContent);
|
|
82
|
+
// The capability as the delta spelled it — messages about the delta's own
|
|
83
|
+
// content echo the author's spelling so they can find it in their file.
|
|
80
84
|
const specName = path.basename(path.dirname(update.target));
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (modifiedNames.has(name)) {
|
|
94
|
-
throw new Error(`${specName} validation failed - duplicate requirement in MODIFIED for header "### Requirement: ${mod.name}"`);
|
|
95
|
-
}
|
|
96
|
-
modifiedNames.add(name);
|
|
97
|
-
}
|
|
98
|
-
const removedNamesSet = new Set();
|
|
99
|
-
for (const rem of plan.removed) {
|
|
100
|
-
const name = normalizeRequirementName(rem);
|
|
101
|
-
if (removedNamesSet.has(name)) {
|
|
102
|
-
throw new Error(`${specName} validation failed - duplicate requirement in REMOVED for header "### Requirement: ${rem}"`);
|
|
85
|
+
// The capability as it exists on disk, which differs on a case-insensitive
|
|
86
|
+
// filesystem: a delta under `specs/RegexCap/` resolves to the existing
|
|
87
|
+
// `tospec/specs/regexcap/`.
|
|
88
|
+
const onDiskSpecName = await resolveOnDiskSpecName(update.target, specName);
|
|
89
|
+
const collectUnique = (names, section) => {
|
|
90
|
+
const seen = new Set();
|
|
91
|
+
for (const original of names) {
|
|
92
|
+
const name = normalizeRequirementName(original);
|
|
93
|
+
if (seen.has(name)) {
|
|
94
|
+
throw new Error(`${specName} validation failed - duplicate requirement in ${section} for header "### Requirement: ${original}"`);
|
|
95
|
+
}
|
|
96
|
+
seen.add(name);
|
|
103
97
|
}
|
|
104
|
-
|
|
105
|
-
}
|
|
98
|
+
return seen;
|
|
99
|
+
};
|
|
100
|
+
const addedNames = collectUnique(plan.added.map((add) => add.name), 'ADDED');
|
|
101
|
+
const modifiedNames = collectUnique(plan.modified.map((mod) => mod.name), 'MODIFIED');
|
|
102
|
+
const removedNamesSet = collectUnique(plan.removed, 'REMOVED');
|
|
103
|
+
// Checked per entry rather than via collectUnique: a pair that duplicates
|
|
104
|
+
// both sides must report the FROM/TO the original interleaving would.
|
|
106
105
|
const renamedFromSet = new Set();
|
|
107
106
|
const renamedToSet = new Set();
|
|
108
107
|
for (const { from, to } of plan.renamed) {
|
|
@@ -117,7 +116,6 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
117
116
|
renamedFromSet.add(fromNorm);
|
|
118
117
|
renamedToSet.add(toNorm);
|
|
119
118
|
}
|
|
120
|
-
// Pre-validate cross-section conflicts
|
|
121
119
|
const conflicts = [];
|
|
122
120
|
for (const n of modifiedNames) {
|
|
123
121
|
if (removedNamesSet.has(n))
|
|
@@ -129,15 +127,12 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
129
127
|
if (removedNamesSet.has(n))
|
|
130
128
|
conflicts.push({ name: n, a: 'ADDED', b: 'REMOVED' });
|
|
131
129
|
}
|
|
132
|
-
// Renamed interplay: MODIFIED must reference the NEW header, not FROM
|
|
133
130
|
for (const { from, to } of plan.renamed) {
|
|
134
131
|
const fromNorm = normalizeRequirementName(from);
|
|
135
132
|
const toNorm = normalizeRequirementName(to);
|
|
136
|
-
// A REMOVED naming the FROM side contradicts the rename.
|
|
137
|
-
// REMOVED target is
|
|
138
|
-
//
|
|
139
|
-
// Compared folded, so a case/whitespace variant cannot slip past into a
|
|
140
|
-
// warned no-op.
|
|
133
|
+
// A REMOVED naming the FROM side contradicts the rename. Since a missing
|
|
134
|
+
// REMOVED target is a no-op (early-sync), this must be rejected explicitly.
|
|
135
|
+
// Folded, so a case or whitespace variant cannot slip past as a no-op.
|
|
141
136
|
const removedFoldMatch = [...removedNamesSet].find((r) => foldRequirementName(r) === foldRequirementName(fromNorm));
|
|
142
137
|
if (removedFoldMatch !== undefined) {
|
|
143
138
|
throw new Error(`${specName} validation failed - requirement present in multiple sections (RENAMED and REMOVED) for header "### Requirement: ${from}"` +
|
|
@@ -146,7 +141,6 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
146
141
|
if (modifiedNames.has(fromNorm)) {
|
|
147
142
|
throw new Error(`${specName} validation failed - when a rename exists, MODIFIED must reference the NEW header "### Requirement: ${to}"`);
|
|
148
143
|
}
|
|
149
|
-
// Detect ADDED colliding with a RENAMED TO
|
|
150
144
|
if (addedNames.has(toNorm)) {
|
|
151
145
|
throw new Error(`${specName} validation failed - RENAMED TO header collides with ADDED for "### Requirement: ${to}"`);
|
|
152
146
|
}
|
|
@@ -160,52 +154,55 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
160
154
|
throw new Error(`Delta parsing found no operations for ${path.basename(path.dirname(update.source))}. ` +
|
|
161
155
|
`Provide ADDED/MODIFIED/REMOVED/RENAMED sections in change spec.`);
|
|
162
156
|
}
|
|
163
|
-
// Load or create base target content
|
|
164
157
|
let targetContent;
|
|
165
158
|
let isNewSpec = false;
|
|
166
159
|
try {
|
|
167
160
|
targetContent = await fs.readFile(update.target, 'utf-8');
|
|
168
161
|
}
|
|
169
162
|
catch (error) {
|
|
170
|
-
// Only "
|
|
171
|
-
//
|
|
172
|
-
// says the spec may well exist with content this path is about to discard.
|
|
173
|
-
// For an addition-only delta that is silent data loss: the new-capability
|
|
174
|
-
// branch below accepts it and rebuilds the spec from a skeleton.
|
|
163
|
+
// Only "not there" means a new capability. Any other failure says the spec
|
|
164
|
+
// may exist with content the skeleton rebuild below would discard.
|
|
175
165
|
if (!isMissingPathError(error))
|
|
176
166
|
throw error;
|
|
177
|
-
// Target spec does not exist; MODIFIED and RENAMED are not allowed for new specs
|
|
178
|
-
// REMOVED will be ignored with a warning since there's nothing to remove
|
|
179
167
|
if (plan.modified.length > 0 || plan.renamed.length > 0) {
|
|
180
168
|
throw new Error(`${specName}: target spec does not exist; only ADDED requirements are allowed for new specs. MODIFIED and RENAMED operations require an existing spec.`);
|
|
181
169
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
console.log(`Warning: ${specName} - ${plan.removed.length} REMOVED requirement(s) ignored for new spec (nothing to remove).`);
|
|
170
|
+
if (plan.removed.length > 0) {
|
|
171
|
+
notice('archive_removed_ignored_new_spec', `${specName} - ${plan.removed.length} REMOVED requirement(s) ignored for new spec (nothing to remove).`);
|
|
185
172
|
}
|
|
186
173
|
isNewSpec = true;
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
// Purpose that could restructure the spec falls back to the placeholder.
|
|
174
|
+
// Only plain prose is carried; a Purpose that could restructure the spec
|
|
175
|
+
// falls back to the placeholder.
|
|
190
176
|
const deltaPurpose = extractDeltaPurpose(changeContent);
|
|
191
177
|
if (deltaPurpose && isPurposeCarrySafe(deltaPurpose)) {
|
|
192
178
|
targetContent = buildSpecSkeleton(specName, changeName, deltaPurpose);
|
|
193
179
|
}
|
|
194
180
|
else {
|
|
195
|
-
if (deltaPurpose
|
|
196
|
-
|
|
181
|
+
if (deltaPurpose) {
|
|
182
|
+
notice('archive_delta_purpose_ignored', `${specName} - delta Purpose ignored (it would leave the new spec unreadable); wrote the placeholder Purpose instead.`);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
// The schema calls `## Purpose` mandatory for a new capability but
|
|
186
|
+
// validation does not check it, so the placeholder reaches the merged
|
|
187
|
+
// spec as a literal "TBD". This is the last moment the change that owns
|
|
188
|
+
// the answer is still in hand.
|
|
189
|
+
notice('archive_purpose_placeholder', `${specName} - delta has no "## Purpose"; wrote a TBD placeholder into the new main spec. Edit tospec/specs/${specName}/spec.md to describe why this capability exists.`);
|
|
197
190
|
}
|
|
198
191
|
targetContent = buildSpecSkeleton(specName, changeName);
|
|
199
192
|
}
|
|
200
193
|
}
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
194
|
+
// An existing capability's main spec owns its Purpose, so a delta's is
|
|
195
|
+
// dropped on this path — an author who wrote one must hear it went nowhere.
|
|
196
|
+
if (!isNewSpec) {
|
|
197
|
+
const strayPurpose = extractDeltaPurpose(changeContent);
|
|
198
|
+
if (strayPurpose) {
|
|
199
|
+
notice('archive_delta_purpose_ignored', `${specName} - delta "## Purpose" ignored; ${onDiskSpecName} already has a main spec and owns its Purpose. Edit tospec/specs/${onDiskSpecName}/spec.md directly if it needs changing.`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Recorded on the raw bytes because everything downstream normalizes to LF
|
|
203
|
+
// (every structural regex below assumes it), so the convention has to be
|
|
204
|
+
// carried forward. A whole-file flag, not per-line: a merged spec contains
|
|
205
|
+
// lines that had no original.
|
|
209
206
|
const crlf = targetContent.includes('\r\n');
|
|
210
207
|
const structureIssues = findMainSpecStructureIssues(targetContent);
|
|
211
208
|
if (structureIssues.length > 0) {
|
|
@@ -214,27 +211,22 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
214
211
|
.join('\n');
|
|
215
212
|
throw new Error(`${specName}: target spec is structurally invalid and cannot be updated until fixed:\n${details}`);
|
|
216
213
|
}
|
|
217
|
-
// Extract requirements section and build name->block map
|
|
218
214
|
const parts = extractRequirementsSection(targetContent);
|
|
219
215
|
const nameToBlock = new Map();
|
|
220
216
|
for (const block of parts.bodyBlocks) {
|
|
221
217
|
nameToBlock.set(normalizeRequirementName(block.name), block);
|
|
222
218
|
}
|
|
223
|
-
// The
|
|
224
|
-
// block's key, so `bodyBlocks` alone
|
|
225
|
-
//
|
|
226
|
-
// untouched because the salvage pass below reads the ORIGINAL raw content.
|
|
219
|
+
// The current key for each original block, by position: RENAMED changes a
|
|
220
|
+
// block's key, so `bodyBlocks` alone cannot find it at recompose time.
|
|
221
|
+
// `bodyBlocks` stays untouched because the salvage pass reads the original raw.
|
|
227
222
|
const orderedKeys = parts.bodyBlocks.map((block) => normalizeRequirementName(block.name));
|
|
228
|
-
//
|
|
229
|
-
// RENAMED
|
|
223
|
+
// Order matters: RENAMED → REMOVED → MODIFIED → ADDED.
|
|
230
224
|
let renamedApplied = 0;
|
|
231
225
|
for (const r of plan.renamed) {
|
|
232
226
|
const from = normalizeRequirementName(r.from);
|
|
233
227
|
const to = normalizeRequirementName(r.to);
|
|
234
228
|
if (!nameToBlock.has(from)) {
|
|
235
|
-
// Source gone but target present
|
|
236
|
-
// the baseline (early-sync pattern) - re-applying it is a no-op, not a
|
|
237
|
-
// failure. Only a missing source AND target is a genuine error.
|
|
229
|
+
// Source gone but target present: the rename was already synced.
|
|
238
230
|
if (nameToBlock.has(to)) {
|
|
239
231
|
continue;
|
|
240
232
|
}
|
|
@@ -255,76 +247,88 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
255
247
|
};
|
|
256
248
|
nameToBlock.delete(from);
|
|
257
249
|
nameToBlock.set(to, renamedBlock);
|
|
258
|
-
// delete+set moves the entry to the Map's insertion-order tail,
|
|
259
|
-
//
|
|
260
|
-
// Re-key it in place instead; a chain A->B->C updates the same slot twice.
|
|
250
|
+
// delete+set moves the entry to the Map's insertion-order tail, pushing the
|
|
251
|
+
// requirement to the end of the rebuilt spec. Re-key in place.
|
|
261
252
|
const orderIndex = orderedKeys.indexOf(from);
|
|
262
253
|
if (orderIndex >= 0) {
|
|
263
254
|
orderedKeys[orderIndex] = to;
|
|
264
255
|
}
|
|
265
256
|
renamedApplied++;
|
|
266
257
|
}
|
|
267
|
-
// REMOVED
|
|
268
258
|
let removedApplied = 0;
|
|
269
259
|
// Keys this run actually removed. A renamed requirement also disappears from
|
|
270
|
-
// its old key, so "absent from nameToBlock"
|
|
271
|
-
//
|
|
260
|
+
// its old key, so "absent from nameToBlock" cannot tell the two apart, and
|
|
261
|
+
// salvaging a renamed block's tail would duplicate it.
|
|
272
262
|
const removedKeys = new Set();
|
|
273
263
|
for (const name of plan.removed) {
|
|
274
264
|
const key = normalizeRequirementName(name);
|
|
275
265
|
if (!nameToBlock.has(key)) {
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
// only in case or interior whitespace still being present. That is a typo,
|
|
280
|
-
// and stays a hard abort. For new specs the skip was already warned above.
|
|
266
|
+
// Already gone means the removal was already synced. A near miss — one
|
|
267
|
+
// still present differing only in case or interior whitespace — is a typo
|
|
268
|
+
// instead, and stays a hard abort.
|
|
281
269
|
if (!isNewSpec) {
|
|
282
270
|
const nearMiss = [...nameToBlock.keys()].find((k) => foldRequirementName(k) === foldRequirementName(key));
|
|
283
271
|
if (nearMiss !== undefined) {
|
|
284
272
|
throw new Error(`${specName} REMOVED failed for header "### Requirement: ${name}" - not found, but "### Requirement: ${nameToBlock.get(nearMiss).name}" exists; fix the header to match it exactly`);
|
|
285
273
|
}
|
|
286
|
-
|
|
287
|
-
// warnings[] channel yet. Add one if agent flows need the signal.
|
|
288
|
-
if (!options.silent) {
|
|
289
|
-
console.log(`Warning: ${specName} - REMOVED requirement "${name}" is not in the current spec; treating it as already removed.`);
|
|
290
|
-
}
|
|
274
|
+
notice('archive_removed_requirement_absent', `${specName} - REMOVED requirement "${name}" is not in the current spec; treating it as already removed. Nothing was deleted — if you meant to remove a requirement, check the header spelling against tospec/specs/${onDiskSpecName}/spec.md.`);
|
|
291
275
|
}
|
|
292
|
-
// Skip removal (nothing to remove)
|
|
293
276
|
continue;
|
|
294
277
|
}
|
|
295
278
|
nameToBlock.delete(key);
|
|
296
279
|
removedKeys.add(key);
|
|
297
280
|
removedApplied++;
|
|
298
281
|
}
|
|
299
|
-
// MODIFIED
|
|
282
|
+
// Declarations are consumed as a MODIFIED omission matches them. A declaration
|
|
283
|
+
// still here afterwards removed nothing: the section grants no removal of its
|
|
284
|
+
// own, so without a matching omission the scenario stays in the spec while
|
|
285
|
+
// the change says it went. The validator reports the same leftover as an
|
|
286
|
+
// ERROR; re-checking here keeps `--no-validate` from archiving what validate
|
|
287
|
+
// refuses.
|
|
288
|
+
const unmatchedDeclarations = new Set(plan.removedScenarios);
|
|
300
289
|
for (const mod of plan.modified) {
|
|
301
290
|
const key = normalizeRequirementName(mod.name);
|
|
302
291
|
const currentBlock = nameToBlock.get(key);
|
|
303
292
|
if (!currentBlock) {
|
|
304
293
|
throw new Error(`${specName} MODIFIED failed for header "### Requirement: ${mod.name}" - not found`);
|
|
305
294
|
}
|
|
306
|
-
|
|
307
|
-
const modHeaderMatch = mod.raw.split('\n')[0].match(/^###\s*Requirement:\s*(.+)\s*$/i);
|
|
295
|
+
const modHeaderMatch = mod.raw.split('\n')[0].match(REQUIREMENT_HEADER_REGEX);
|
|
308
296
|
if (!modHeaderMatch || normalizeRequirementName(modHeaderMatch[1]) !== key) {
|
|
309
297
|
throw new Error(`${specName} MODIFIED failed for header "### Requirement: ${mod.name}" - header mismatch in content`);
|
|
310
298
|
}
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
|
|
299
|
+
// Guards against a stale MODIFIED block dropping scenarios a previous
|
|
300
|
+
// archive added. `## REMOVED Scenarios` exempts them, read from the same
|
|
301
|
+
// declarations the validator uses so the two cannot disagree.
|
|
302
|
+
const declaredRemovals = new Set(plan.removedScenarios.filter((entry) => entry.requirement === key).map((entry) => entry.scenario));
|
|
303
|
+
const missingScenarios = findMissingScenarios(currentBlock.raw, mod.raw).filter((name) => {
|
|
304
|
+
if (!declaredRemovals.has(name))
|
|
305
|
+
return true;
|
|
306
|
+
for (const entry of unmatchedDeclarations) {
|
|
307
|
+
if (entry.requirement === key && entry.scenario === name)
|
|
308
|
+
unmatchedDeclarations.delete(entry);
|
|
309
|
+
}
|
|
310
|
+
return false;
|
|
311
|
+
});
|
|
314
312
|
if (missingScenarios.length > 0) {
|
|
315
313
|
throw new Error(`${specName} MODIFIED failed for header "### Requirement: ${mod.name}" - current spec contains scenario(s) not present in the modified block: ${missingScenarios.map(name => `"${name}"`).join(', ')}. Refresh the change spec before archiving to avoid dropping scenarios.`);
|
|
316
314
|
}
|
|
317
|
-
|
|
315
|
+
// A plain `### Notes` after the requirement is absorbed into its block (see
|
|
316
|
+
// the recompose pass below), and replacing the block wholesale deleted it
|
|
317
|
+
// with the old body. Same salvage as REMOVED, appended after the new body
|
|
318
|
+
// so the section keeps its place.
|
|
319
|
+
const absorbed = salvageAbsorbedContent(currentBlock.raw);
|
|
320
|
+
nameToBlock.set(key, absorbed ? { ...mod, raw: `${mod.raw.trimEnd()}\n\n${absorbed}` } : mod);
|
|
321
|
+
}
|
|
322
|
+
for (const entry of unmatchedDeclarations) {
|
|
323
|
+
throw new Error(`${specName} REMOVED Scenarios declares "${entry.scenario}" under requirement "${entry.requirement}", but no MODIFIED block for that requirement drops that scenario - the declaration removes nothing on its own. Restate the requirement under "## MODIFIED Requirements" without that scenario, or check both names against the current spec.`);
|
|
318
324
|
}
|
|
319
|
-
// ADDED
|
|
320
325
|
let addedApplied = 0;
|
|
321
326
|
for (const add of plan.added) {
|
|
322
327
|
const key = normalizeRequirementName(add.name);
|
|
323
328
|
const existing = nameToBlock.get(key);
|
|
324
329
|
if (existing) {
|
|
325
|
-
// Identical content means
|
|
326
|
-
//
|
|
327
|
-
// conflict. Only differing content is a genuine collision.
|
|
330
|
+
// Identical content means it was already synced; only differing content
|
|
331
|
+
// is a genuine collision.
|
|
328
332
|
if (normalizeBlockRaw(existing.raw) === normalizeBlockRaw(add.raw)) {
|
|
329
333
|
continue;
|
|
330
334
|
}
|
|
@@ -333,13 +337,10 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
333
337
|
nameToBlock.set(key, add);
|
|
334
338
|
addedApplied++;
|
|
335
339
|
}
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
// it. Removing that requirement used to delete the absorbed content with it,
|
|
341
|
-
// silently: nothing counted it, so nothing warned, and the spec left behind
|
|
342
|
-
// still validated. Salvage it back into place instead.
|
|
340
|
+
// Recompose, preserving original ordering. A block's `raw` runs to the next
|
|
341
|
+
// header the parser recognises, so one it does not — a plain `### Notes` — is
|
|
342
|
+
// absorbed into the requirement above, and removing that requirement would
|
|
343
|
+
// delete the absorbed content silently. Salvage it back into place instead.
|
|
343
344
|
const pieces = [];
|
|
344
345
|
const seen = new Set();
|
|
345
346
|
let salvagedCount = 0;
|
|
@@ -363,7 +364,7 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
363
364
|
}
|
|
364
365
|
}
|
|
365
366
|
}
|
|
366
|
-
//
|
|
367
|
+
// Whatever was added rather than replacing an original slot.
|
|
367
368
|
for (const [key, block] of nameToBlock.entries()) {
|
|
368
369
|
if (!seen.has(key)) {
|
|
369
370
|
pieces.push(block.raw);
|
|
@@ -371,8 +372,7 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
371
372
|
}
|
|
372
373
|
}
|
|
373
374
|
// Every requirement is gone but absorbed content survived. Retiring would
|
|
374
|
-
// delete that content
|
|
375
|
-
// salvage exists to prevent; writing it would produce a spec with no
|
|
375
|
+
// delete that content with the file; writing it would produce a spec with no
|
|
376
376
|
// requirements, which cannot validate. Neither is safe to choose silently.
|
|
377
377
|
if (keptRequirements === 0 && removedApplied > 0 && salvagedCount > 0) {
|
|
378
378
|
throw new Error(`${specName}: removing every requirement would leave content that is not part of any requirement (e.g. a "### Notes" section). Move that content out of ${specName}/spec.md first, then archive again.`);
|
|
@@ -382,27 +382,21 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
382
382
|
.concat(pieces)
|
|
383
383
|
.join('\n\n')
|
|
384
384
|
.trimEnd();
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
// the heading to the Purpose paragraph above it and the first requirement
|
|
389
|
-
// below, so every archive rewrote a well-formatted spec into that shape.
|
|
390
|
-
// Separate non-empty slices with one blank line, and end the file with
|
|
391
|
-
// exactly one newline instead of inheriting whatever `after` happened to hold.
|
|
385
|
+
// Every slice is trimmed, so the blank lines around `## Requirements` belong
|
|
386
|
+
// to no slice: joining with a bare '\n' glues the heading to the Purpose above
|
|
387
|
+
// and the first requirement below.
|
|
392
388
|
const rebuilt = collapseBlankRunsOutsideFences([parts.before.trimEnd(), parts.headerLine, reqBody, parts.after.trim()]
|
|
393
389
|
.filter((s) => s !== '')
|
|
394
390
|
.join('\n\n')).trimEnd() + '\n';
|
|
395
391
|
return {
|
|
396
392
|
rebuilt,
|
|
397
393
|
crlf,
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
// requirements. An emptied spec cannot be written either way — it fails
|
|
403
|
-
// validation ("Spec must have at least one requirement") — so the caller
|
|
404
|
-
// retires the capability instead of writing an unusable file.
|
|
394
|
+
// Both halves matter: "no requirements left" alone would delete a spec on a
|
|
395
|
+
// re-run of an already-synced REMOVED; "removed something" alone would
|
|
396
|
+
// delete a spec that still has requirements. An emptied spec cannot be
|
|
397
|
+
// written (it fails "at least one requirement"), so the caller retires it.
|
|
405
398
|
retired: keptRequirements === 0 && removedApplied > 0,
|
|
399
|
+
notices,
|
|
406
400
|
counts: {
|
|
407
401
|
added: addedApplied,
|
|
408
402
|
modified: plan.modified.length,
|
|
@@ -412,18 +406,13 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
412
406
|
};
|
|
413
407
|
}
|
|
414
408
|
function normalizeBlockRaw(raw) {
|
|
415
|
-
return raw
|
|
409
|
+
return normalizeDocument(raw).trim();
|
|
416
410
|
}
|
|
417
411
|
/**
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
* to the whole document as `replace(/\n{3,}/g, '\n\n')` it could not tell a seam
|
|
423
|
-
* from a blank line an author put inside a code sample deliberately, so every
|
|
424
|
-
* archive quietly edited the examples in a spec. The seams are all outside any
|
|
425
|
-
* fence by construction, so masking fenced lines loses nothing the collapse was
|
|
426
|
-
* written for.
|
|
412
|
+
* The slices joined above each carry boundary newlines, so their seams
|
|
413
|
+
* accumulate blank lines. A whole-document `replace(/\n{3,}/g, '\n\n')` cannot
|
|
414
|
+
* tell a seam from a deliberate blank line inside a code sample. Seams are all
|
|
415
|
+
* outside fences by construction, so masking loses nothing.
|
|
427
416
|
*/
|
|
428
417
|
function collapseBlankRunsOutsideFences(content) {
|
|
429
418
|
const lines = content.split('\n');
|
|
@@ -433,9 +422,8 @@ function collapseBlankRunsOutsideFences(content) {
|
|
|
433
422
|
for (let i = 0; i < lines.length; i++) {
|
|
434
423
|
if (!mask[i] && lines[i].trim() === '') {
|
|
435
424
|
const last = kept.length - 1;
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
// not license dropping the one that follows it.
|
|
425
|
+
// The previous blank must also be outside a fence: a blank inside one is
|
|
426
|
+
// content, and must not license dropping the line that follows it.
|
|
439
427
|
if (last >= 0 && !keptMask[last] && kept[last].trim() === '')
|
|
440
428
|
continue;
|
|
441
429
|
}
|
|
@@ -444,16 +432,11 @@ function collapseBlankRunsOutsideFences(content) {
|
|
|
444
432
|
}
|
|
445
433
|
return kept.join('\n');
|
|
446
434
|
}
|
|
447
|
-
/**
|
|
448
|
-
* Write an updated spec to disk.
|
|
449
|
-
*/
|
|
450
435
|
export async function writeUpdatedSpec(update, rebuilt, counts, options = {}) {
|
|
451
|
-
// Create target directory if needed
|
|
452
436
|
const targetDir = path.dirname(update.target);
|
|
453
437
|
await fs.mkdir(targetDir, { recursive: true });
|
|
454
|
-
// `rebuilt` is pure LF by construction; restore the
|
|
455
|
-
//
|
|
456
|
-
// on Windows. A brand new spec has no prior convention and stays LF.
|
|
438
|
+
// `rebuilt` is pure LF by construction; restore the target's pre-merge
|
|
439
|
+
// convention so archiving a CRLF spec is not a whole-file diff on Windows.
|
|
457
440
|
const content = options.crlf ? rebuilt.replace(/\r?\n/g, '\r\n') : rebuilt;
|
|
458
441
|
await fs.writeFile(update.target, content);
|
|
459
442
|
if (options.silent)
|
|
@@ -471,19 +454,15 @@ export async function writeUpdatedSpec(update, rebuilt, counts, options = {}) {
|
|
|
471
454
|
}
|
|
472
455
|
/**
|
|
473
456
|
* The tail of a requirement block that is not part of the requirement: content
|
|
474
|
-
* from the first `#`/`##`/`###` heading after
|
|
475
|
-
*
|
|
476
|
-
* `####` is excluded deliberately — a requirement's `#### Scenario:` headings
|
|
477
|
-
* are its own and go with it when it is removed.
|
|
457
|
+
* from the first `#`/`##`/`###` heading after its own header line. `####` is
|
|
458
|
+
* excluded — a requirement's `#### Scenario:` headings go with it.
|
|
478
459
|
*
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
* a spec that was valid becomes invalid. Salvaging on removal changes what a
|
|
483
|
-
* removal deletes without changing what anything parses.
|
|
460
|
+
* Widening the parsers' heading patterns instead was rejected: it reclassifies
|
|
461
|
+
* content, so commented-out or indented examples begin parsing as requirements.
|
|
462
|
+
* Salvaging changes only what a removal deletes, not what anything parses.
|
|
484
463
|
*/
|
|
485
464
|
function salvageAbsorbedContent(requirementRaw) {
|
|
486
|
-
const lines = requirementRaw
|
|
465
|
+
const lines = normalizeDocument(requirementRaw).split('\n');
|
|
487
466
|
const mask = buildCodeFenceMask(lines);
|
|
488
467
|
for (let i = 1; i < lines.length; i++) {
|
|
489
468
|
if (mask[i])
|
|
@@ -495,12 +474,9 @@ function salvageAbsorbedContent(requirementRaw) {
|
|
|
495
474
|
return '';
|
|
496
475
|
}
|
|
497
476
|
/**
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
* The pruning walks up from the spec file and stops at `specsRoot`, which is
|
|
502
|
-
* never removed even if it ends up empty — a project with no capabilities still
|
|
503
|
-
* has a specs directory. It also stops at the first non-empty directory, so a
|
|
477
|
+
* Delete an emptied capability's `spec.md` and prune the directories that
|
|
478
|
+
* leaves empty. Pruning stops at `specsRoot` (a project with no capabilities
|
|
479
|
+
* still has a specs directory) and at the first non-empty directory, so a
|
|
504
480
|
* sibling capability is never touched.
|
|
505
481
|
*/
|
|
506
482
|
export async function retireSpec(update, specsRoot, options = {}) {
|
|
@@ -525,9 +501,8 @@ export async function retireSpec(update, specsRoot, options = {}) {
|
|
|
525
501
|
console.log(`Retiring ${options.displayPath ?? `tospec/specs/${specName}/spec.md`}: no requirements left`);
|
|
526
502
|
}
|
|
527
503
|
/**
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
* from the constants validation recognises it by, so the two cannot drift.
|
|
504
|
+
* Skeleton spec for a new capability. The TBD placeholder is composed from the
|
|
505
|
+
* constants validation recognises it by, so the two cannot drift.
|
|
531
506
|
*/
|
|
532
507
|
export function buildSpecSkeleton(specFolderName, changeName, purpose) {
|
|
533
508
|
const titleBase = specFolderName;
|
|
@@ -541,8 +516,10 @@ export function buildSpecSkeleton(specFolderName, changeName, purpose) {
|
|
|
541
516
|
* is none. Fence-masked so a `## Purpose` inside a code example is not mistaken
|
|
542
517
|
* for the real section.
|
|
543
518
|
*/
|
|
544
|
-
function extractDeltaPurpose(content) {
|
|
545
|
-
|
|
519
|
+
export function extractDeltaPurpose(content) {
|
|
520
|
+
// normalizeDocument, not a hand-rolled newline collapse: it also strips the
|
|
521
|
+
// BOM, without which `<BOM>## Purpose` misses the `^##` anchor.
|
|
522
|
+
const lines = normalizeDocument(content).split('\n');
|
|
546
523
|
const mask = buildCodeFenceMask(lines);
|
|
547
524
|
let start = -1;
|
|
548
525
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -567,13 +544,9 @@ function extractDeltaPurpose(content) {
|
|
|
567
544
|
return lines.slice(start, end).join('\n').trim();
|
|
568
545
|
}
|
|
569
546
|
/**
|
|
570
|
-
* A carried Purpose must not restructure the new spec
|
|
571
|
-
* and code fences each
|
|
572
|
-
*
|
|
573
|
-
* it.
|
|
574
|
-
* ponytail: prose-only heuristic; a Purpose that genuinely needs a fence or
|
|
575
|
-
* heading falls back to TBD. Loosen only once those corruption classes are
|
|
576
|
-
* handled the way archive's own validator reads the spec.
|
|
547
|
+
* A carried Purpose must not restructure the new spec: headings, HTML comments
|
|
548
|
+
* and code fences can each corrupt or hide the rest of it, so any of them falls
|
|
549
|
+
* back to the placeholder.
|
|
577
550
|
*/
|
|
578
551
|
function isPurposeCarrySafe(body) {
|
|
579
552
|
if (!body)
|