@seanmars/tospec 0.19.0-beta.0 → 0.19.0-beta.13
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 +578 -0
- package/README.md +71 -77
- package/assets/dashboard/app.js +14 -3
- package/assets/dashboard/style.css +7 -0
- package/assets/metrics/app.js +22 -0
- package/assets/metrics/style.css +8 -0
- package/assets/rules/tospec/decision.md +3 -0
- package/assets/rules/tospec/single-source-of-truth.md +19 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +266 -82
- 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 +374 -95
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/dashboard.d.ts +64 -81
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dashboard.js +350 -218
- 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 +400 -70
- package/dist/commands/decision.js.map +1 -1
- package/dist/commands/metrics.d.ts +34 -48
- package/dist/commands/metrics.d.ts.map +1 -1
- package/dist/commands/metrics.js +74 -84
- package/dist/commands/metrics.js.map +1 -1
- package/dist/commands/shared-output.d.ts +24 -10
- package/dist/commands/shared-output.d.ts.map +1 -1
- package/dist/commands/shared-output.js +62 -11
- 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 +39 -8
- package/dist/commands/show.js.map +1 -1
- package/dist/commands/validate.d.ts +47 -30
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +282 -108
- 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 +84 -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 +415 -201
- package/dist/core/archive.js.map +1 -1
- package/dist/core/artifact-graph/graph.d.ts +29 -36
- package/dist/core/artifact-graph/graph.d.ts.map +1 -1
- package/dist/core/artifact-graph/graph.js +50 -58
- package/dist/core/artifact-graph/graph.js.map +1 -1
- package/dist/core/artifact-graph/index.d.ts +2 -2
- package/dist/core/artifact-graph/index.d.ts.map +1 -1
- package/dist/core/artifact-graph/index.js +2 -2
- package/dist/core/artifact-graph/index.js.map +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +105 -100
- package/dist/core/artifact-graph/instruction-loader.d.ts.map +1 -1
- package/dist/core/artifact-graph/instruction-loader.js +176 -110
- package/dist/core/artifact-graph/instruction-loader.js.map +1 -1
- package/dist/core/artifact-graph/outputs.d.ts +13 -6
- package/dist/core/artifact-graph/outputs.d.ts.map +1 -1
- package/dist/core/artifact-graph/outputs.js +146 -8
- 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 +22 -0
- package/dist/core/codex-residue.d.ts.map +1 -0
- package/dist/core/codex-residue.js +61 -0
- package/dist/core/codex-residue.js.map +1 -0
- 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 +56 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +73 -2
- package/dist/core/config.js.map +1 -1
- package/dist/core/converters/json-converter.d.ts.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 +84 -68
- 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 +43 -62
- package/dist/core/global-config.js.map +1 -1
- package/dist/core/init.d.ts +29 -11
- package/dist/core/init.d.ts.map +1 -1
- package/dist/core/init.js +232 -164
- 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 +63 -39
- package/dist/core/local-server.d.ts.map +1 -1
- package/dist/core/local-server.js +99 -53
- 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 +32 -15
- package/dist/core/migrate.d.ts.map +1 -1
- package/dist/core/migrate.js +220 -108
- 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 +53 -11
- package/dist/core/parsers/requirement-blocks.d.ts.map +1 -1
- package/dist/core/parsers/requirement-blocks.js +200 -60
- 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 +1 -1
- package/dist/core/parsers/spec-structure.d.ts.map +1 -1
- package/dist/core/parsers/spec-structure.js +30 -3
- package/dist/core/parsers/spec-structure.js.map +1 -1
- package/dist/core/planning-home.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 +10 -18
- 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 +10 -0
- package/dist/core/rules.d.ts.map +1 -0
- package/dist/core/rules.js +43 -0
- package/dist/core/rules.js.map +1 -0
- 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 +27 -8
- package/dist/core/shared/rules-generation.d.ts.map +1 -1
- package/dist/core/shared/rules-generation.js +151 -16
- package/dist/core/shared/rules-generation.js.map +1 -1
- package/dist/core/shared/skill-generation.d.ts +38 -53
- 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 +40 -62
- 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.d.ts.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 +24 -23
- package/dist/core/specs-apply.d.ts.map +1 -1
- package/dist/core/specs-apply.js +188 -173
- 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/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 +9 -12
- 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 +7 -5
- 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 -0
- 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 +0 -1
- 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 +2 -2
- 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.map +1 -1
- package/dist/core/update.d.ts +26 -21
- package/dist/core/update.d.ts.map +1 -1
- package/dist/core/update.js +165 -116
- 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 +19 -25
- 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 +50 -51
- package/dist/core/validation/validator.d.ts.map +1 -1
- package/dist/core/validation/validator.js +486 -263
- 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 +31 -54
- package/dist/utils/change-utils.d.ts.map +1 -1
- package/dist/utils/change-utils.js +143 -100
- 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 +13 -4
- package/dist/utils/link.d.ts.map +1 -1
- package/dist/utils/link.js +13 -4
- package/dist/utils/link.js.map +1 -1
- package/dist/utils/match.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 +10 -11
- package/dist/utils/spec-files.d.ts.map +1 -1
- package/dist/utils/spec-files.js +31 -22
- 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 +9 -10
- package/schemas/decision/templates/decision.md +3 -1
- package/schemas/decision/templates/index.md +2 -2
- package/schemas/issue/schema.yaml +11 -2
- package/schemas/issue/templates/spec.md +37 -3
- package/schemas/sdd/schema.yaml +24 -1
- package/schemas/sdd/templates/spec.md +37 -3
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,44 +154,56 @@ 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
|
}
|
|
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.
|
|
206
|
+
const crlf = targetContent.includes('\r\n');
|
|
201
207
|
const structureIssues = findMainSpecStructureIssues(targetContent);
|
|
202
208
|
if (structureIssues.length > 0) {
|
|
203
209
|
const details = structureIssues
|
|
@@ -205,27 +211,22 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
205
211
|
.join('\n');
|
|
206
212
|
throw new Error(`${specName}: target spec is structurally invalid and cannot be updated until fixed:\n${details}`);
|
|
207
213
|
}
|
|
208
|
-
// Extract requirements section and build name->block map
|
|
209
214
|
const parts = extractRequirementsSection(targetContent);
|
|
210
215
|
const nameToBlock = new Map();
|
|
211
216
|
for (const block of parts.bodyBlocks) {
|
|
212
217
|
nameToBlock.set(normalizeRequirementName(block.name), block);
|
|
213
218
|
}
|
|
214
|
-
// The
|
|
215
|
-
// block's key, so `bodyBlocks` alone
|
|
216
|
-
//
|
|
217
|
-
// 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.
|
|
218
222
|
const orderedKeys = parts.bodyBlocks.map((block) => normalizeRequirementName(block.name));
|
|
219
|
-
//
|
|
220
|
-
// RENAMED
|
|
223
|
+
// Order matters: RENAMED → REMOVED → MODIFIED → ADDED.
|
|
221
224
|
let renamedApplied = 0;
|
|
222
225
|
for (const r of plan.renamed) {
|
|
223
226
|
const from = normalizeRequirementName(r.from);
|
|
224
227
|
const to = normalizeRequirementName(r.to);
|
|
225
228
|
if (!nameToBlock.has(from)) {
|
|
226
|
-
// Source gone but target present
|
|
227
|
-
// the baseline (early-sync pattern) - re-applying it is a no-op, not a
|
|
228
|
-
// failure. Only a missing source AND target is a genuine error.
|
|
229
|
+
// Source gone but target present: the rename was already synced.
|
|
229
230
|
if (nameToBlock.has(to)) {
|
|
230
231
|
continue;
|
|
231
232
|
}
|
|
@@ -246,76 +247,88 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
246
247
|
};
|
|
247
248
|
nameToBlock.delete(from);
|
|
248
249
|
nameToBlock.set(to, renamedBlock);
|
|
249
|
-
// delete+set moves the entry to the Map's insertion-order tail,
|
|
250
|
-
//
|
|
251
|
-
// 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.
|
|
252
252
|
const orderIndex = orderedKeys.indexOf(from);
|
|
253
253
|
if (orderIndex >= 0) {
|
|
254
254
|
orderedKeys[orderIndex] = to;
|
|
255
255
|
}
|
|
256
256
|
renamedApplied++;
|
|
257
257
|
}
|
|
258
|
-
// REMOVED
|
|
259
258
|
let removedApplied = 0;
|
|
260
259
|
// Keys this run actually removed. A renamed requirement also disappears from
|
|
261
|
-
// its old key, so "absent from nameToBlock"
|
|
262
|
-
//
|
|
260
|
+
// its old key, so "absent from nameToBlock" cannot tell the two apart, and
|
|
261
|
+
// salvaging a renamed block's tail would duplicate it.
|
|
263
262
|
const removedKeys = new Set();
|
|
264
263
|
for (const name of plan.removed) {
|
|
265
264
|
const key = normalizeRequirementName(name);
|
|
266
265
|
if (!nameToBlock.has(key)) {
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
// only in case or interior whitespace still being present. That is a typo,
|
|
271
|
-
// 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.
|
|
272
269
|
if (!isNewSpec) {
|
|
273
270
|
const nearMiss = [...nameToBlock.keys()].find((k) => foldRequirementName(k) === foldRequirementName(key));
|
|
274
271
|
if (nearMiss !== undefined) {
|
|
275
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`);
|
|
276
273
|
}
|
|
277
|
-
|
|
278
|
-
// warnings[] channel yet. Add one if agent flows need the signal.
|
|
279
|
-
if (!options.silent) {
|
|
280
|
-
console.log(`Warning: ${specName} - REMOVED requirement "${name}" is not in the current spec; treating it as already removed.`);
|
|
281
|
-
}
|
|
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.`);
|
|
282
275
|
}
|
|
283
|
-
// Skip removal (nothing to remove)
|
|
284
276
|
continue;
|
|
285
277
|
}
|
|
286
278
|
nameToBlock.delete(key);
|
|
287
279
|
removedKeys.add(key);
|
|
288
280
|
removedApplied++;
|
|
289
281
|
}
|
|
290
|
-
// 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);
|
|
291
289
|
for (const mod of plan.modified) {
|
|
292
290
|
const key = normalizeRequirementName(mod.name);
|
|
293
291
|
const currentBlock = nameToBlock.get(key);
|
|
294
292
|
if (!currentBlock) {
|
|
295
293
|
throw new Error(`${specName} MODIFIED failed for header "### Requirement: ${mod.name}" - not found`);
|
|
296
294
|
}
|
|
297
|
-
|
|
298
|
-
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);
|
|
299
296
|
if (!modHeaderMatch || normalizeRequirementName(modHeaderMatch[1]) !== key) {
|
|
300
297
|
throw new Error(`${specName} MODIFIED failed for header "### Requirement: ${mod.name}" - header mismatch in content`);
|
|
301
298
|
}
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
|
|
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
|
+
});
|
|
305
312
|
if (missingScenarios.length > 0) {
|
|
306
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.`);
|
|
307
314
|
}
|
|
308
|
-
|
|
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.`);
|
|
309
324
|
}
|
|
310
|
-
// ADDED
|
|
311
325
|
let addedApplied = 0;
|
|
312
326
|
for (const add of plan.added) {
|
|
313
327
|
const key = normalizeRequirementName(add.name);
|
|
314
328
|
const existing = nameToBlock.get(key);
|
|
315
329
|
if (existing) {
|
|
316
|
-
// Identical content means
|
|
317
|
-
//
|
|
318
|
-
// conflict. Only differing content is a genuine collision.
|
|
330
|
+
// Identical content means it was already synced; only differing content
|
|
331
|
+
// is a genuine collision.
|
|
319
332
|
if (normalizeBlockRaw(existing.raw) === normalizeBlockRaw(add.raw)) {
|
|
320
333
|
continue;
|
|
321
334
|
}
|
|
@@ -324,13 +337,10 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
324
337
|
nameToBlock.set(key, add);
|
|
325
338
|
addedApplied++;
|
|
326
339
|
}
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
// it. Removing that requirement used to delete the absorbed content with it,
|
|
332
|
-
// silently: nothing counted it, so nothing warned, and the spec left behind
|
|
333
|
-
// 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.
|
|
334
344
|
const pieces = [];
|
|
335
345
|
const seen = new Set();
|
|
336
346
|
let salvagedCount = 0;
|
|
@@ -354,7 +364,7 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
354
364
|
}
|
|
355
365
|
}
|
|
356
366
|
}
|
|
357
|
-
//
|
|
367
|
+
// Whatever was added rather than replacing an original slot.
|
|
358
368
|
for (const [key, block] of nameToBlock.entries()) {
|
|
359
369
|
if (!seen.has(key)) {
|
|
360
370
|
pieces.push(block.raw);
|
|
@@ -362,8 +372,7 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
362
372
|
}
|
|
363
373
|
}
|
|
364
374
|
// Every requirement is gone but absorbed content survived. Retiring would
|
|
365
|
-
// delete that content
|
|
366
|
-
// 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
|
|
367
376
|
// requirements, which cannot validate. Neither is safe to choose silently.
|
|
368
377
|
if (keptRequirements === 0 && removedApplied > 0 && salvagedCount > 0) {
|
|
369
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.`);
|
|
@@ -373,28 +382,21 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
373
382
|
.concat(pieces)
|
|
374
383
|
.join('\n\n')
|
|
375
384
|
.trimEnd();
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
|
|
380
|
-
// below, so every archive rewrote a well-formatted spec into that shape.
|
|
381
|
-
// Separate non-empty slices with one blank line, and end the file with
|
|
382
|
-
// exactly one newline instead of inheriting whatever `after` happened to hold.
|
|
383
|
-
const rebuilt = [parts.before.trimEnd(), parts.headerLine, reqBody, parts.after.trim()]
|
|
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.
|
|
388
|
+
const rebuilt = collapseBlankRunsOutsideFences([parts.before.trimEnd(), parts.headerLine, reqBody, parts.after.trim()]
|
|
384
389
|
.filter((s) => s !== '')
|
|
385
|
-
.join('\n\n')
|
|
386
|
-
.replace(/\n{3,}/g, '\n\n')
|
|
387
|
-
.trimEnd() + '\n';
|
|
390
|
+
.join('\n\n')).trimEnd() + '\n';
|
|
388
391
|
return {
|
|
389
392
|
rebuilt,
|
|
390
|
-
|
|
391
|
-
// Both halves matter
|
|
392
|
-
// re-run of an already-synced REMOVED
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
// validation ("Spec must have at least one requirement") — so the caller
|
|
396
|
-
// retires the capability instead of writing an unusable file.
|
|
393
|
+
crlf,
|
|
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.
|
|
397
398
|
retired: keptRequirements === 0 && removedApplied > 0,
|
|
399
|
+
notices,
|
|
398
400
|
counts: {
|
|
399
401
|
added: addedApplied,
|
|
400
402
|
modified: plan.modified.length,
|
|
@@ -404,16 +406,39 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
404
406
|
};
|
|
405
407
|
}
|
|
406
408
|
function normalizeBlockRaw(raw) {
|
|
407
|
-
return raw
|
|
409
|
+
return normalizeDocument(raw).trim();
|
|
408
410
|
}
|
|
409
411
|
/**
|
|
410
|
-
*
|
|
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.
|
|
411
416
|
*/
|
|
417
|
+
function collapseBlankRunsOutsideFences(content) {
|
|
418
|
+
const lines = content.split('\n');
|
|
419
|
+
const mask = buildCodeFenceMask(lines);
|
|
420
|
+
const kept = [];
|
|
421
|
+
const keptMask = [];
|
|
422
|
+
for (let i = 0; i < lines.length; i++) {
|
|
423
|
+
if (!mask[i] && lines[i].trim() === '') {
|
|
424
|
+
const last = kept.length - 1;
|
|
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.
|
|
427
|
+
if (last >= 0 && !keptMask[last] && kept[last].trim() === '')
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
kept.push(lines[i]);
|
|
431
|
+
keptMask.push(mask[i]);
|
|
432
|
+
}
|
|
433
|
+
return kept.join('\n');
|
|
434
|
+
}
|
|
412
435
|
export async function writeUpdatedSpec(update, rebuilt, counts, options = {}) {
|
|
413
|
-
// Create target directory if needed
|
|
414
436
|
const targetDir = path.dirname(update.target);
|
|
415
437
|
await fs.mkdir(targetDir, { recursive: true });
|
|
416
|
-
|
|
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.
|
|
440
|
+
const content = options.crlf ? rebuilt.replace(/\r?\n/g, '\r\n') : rebuilt;
|
|
441
|
+
await fs.writeFile(update.target, content);
|
|
417
442
|
if (options.silent)
|
|
418
443
|
return;
|
|
419
444
|
const specName = path.basename(path.dirname(update.target));
|
|
@@ -429,19 +454,15 @@ export async function writeUpdatedSpec(update, rebuilt, counts, options = {}) {
|
|
|
429
454
|
}
|
|
430
455
|
/**
|
|
431
456
|
* The tail of a requirement block that is not part of the requirement: content
|
|
432
|
-
* from the first `#`/`##`/`###` heading after
|
|
457
|
+
* from the first `#`/`##`/`###` heading after its own header line. `####` is
|
|
458
|
+
* excluded — a requirement's `#### Scenario:` headings go with it.
|
|
433
459
|
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
* The alternative was to widen every heading pattern in the parsers so these
|
|
438
|
-
* headings end the block properly. That was rejected: it reclassifies content,
|
|
439
|
-
* so commented-out or indented examples start parsing as real requirements and
|
|
440
|
-
* a spec that was valid becomes invalid. Salvaging on removal changes what a
|
|
441
|
-
* 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.
|
|
442
463
|
*/
|
|
443
464
|
function salvageAbsorbedContent(requirementRaw) {
|
|
444
|
-
const lines = requirementRaw
|
|
465
|
+
const lines = normalizeDocument(requirementRaw).split('\n');
|
|
445
466
|
const mask = buildCodeFenceMask(lines);
|
|
446
467
|
for (let i = 1; i < lines.length; i++) {
|
|
447
468
|
if (mask[i])
|
|
@@ -453,12 +474,9 @@ function salvageAbsorbedContent(requirementRaw) {
|
|
|
453
474
|
return '';
|
|
454
475
|
}
|
|
455
476
|
/**
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
*
|
|
459
|
-
* The pruning walks up from the spec file and stops at `specsRoot`, which is
|
|
460
|
-
* never removed even if it ends up empty — a project with no capabilities still
|
|
461
|
-
* 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
|
|
462
480
|
* sibling capability is never touched.
|
|
463
481
|
*/
|
|
464
482
|
export async function retireSpec(update, specsRoot, options = {}) {
|
|
@@ -483,9 +501,8 @@ export async function retireSpec(update, specsRoot, options = {}) {
|
|
|
483
501
|
console.log(`Retiring ${options.displayPath ?? `tospec/specs/${specName}/spec.md`}: no requirements left`);
|
|
484
502
|
}
|
|
485
503
|
/**
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
* 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.
|
|
489
506
|
*/
|
|
490
507
|
export function buildSpecSkeleton(specFolderName, changeName, purpose) {
|
|
491
508
|
const titleBase = specFolderName;
|
|
@@ -499,8 +516,10 @@ export function buildSpecSkeleton(specFolderName, changeName, purpose) {
|
|
|
499
516
|
* is none. Fence-masked so a `## Purpose` inside a code example is not mistaken
|
|
500
517
|
* for the real section.
|
|
501
518
|
*/
|
|
502
|
-
function extractDeltaPurpose(content) {
|
|
503
|
-
|
|
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');
|
|
504
523
|
const mask = buildCodeFenceMask(lines);
|
|
505
524
|
let start = -1;
|
|
506
525
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -525,13 +544,9 @@ function extractDeltaPurpose(content) {
|
|
|
525
544
|
return lines.slice(start, end).join('\n').trim();
|
|
526
545
|
}
|
|
527
546
|
/**
|
|
528
|
-
* A carried Purpose must not restructure the new spec
|
|
529
|
-
* and code fences each
|
|
530
|
-
*
|
|
531
|
-
* it.
|
|
532
|
-
* ponytail: prose-only heuristic; a Purpose that genuinely needs a fence or
|
|
533
|
-
* heading falls back to TBD. Loosen only once those corruption classes are
|
|
534
|
-
* 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.
|
|
535
550
|
*/
|
|
536
551
|
function isPurposeCarrySafe(body) {
|
|
537
552
|
if (!body)
|