@seanmars/tospec 0.19.0-beta.8 → 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 -310
- package/README.md +69 -82
- package/assets/dashboard/app.js +11 -0
- package/assets/dashboard/style.css +7 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +89 -106
- 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 +293 -145
- 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 +190 -313
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/decision.d.ts +38 -27
- package/dist/commands/decision.d.ts.map +1 -1
- package/dist/commands/decision.js +296 -128
- 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 +12 -27
- package/dist/commands/shared-output.d.ts.map +1 -1
- package/dist/commands/shared-output.js +22 -45
- package/dist/commands/shared-output.js.map +1 -1
- package/dist/commands/show.d.ts +4 -7
- package/dist/commands/show.d.ts.map +1 -1
- package/dist/commands/show.js +23 -11
- package/dist/commands/show.js.map +1 -1
- package/dist/commands/validate.d.ts +34 -58
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +225 -141
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/workflow/index.d.ts +1 -5
- package/dist/commands/workflow/index.d.ts.map +1 -1
- package/dist/commands/workflow/index.js +1 -5
- package/dist/commands/workflow/index.js.map +1 -1
- package/dist/commands/workflow/instructions.d.ts +14 -24
- package/dist/commands/workflow/instructions.d.ts.map +1 -1
- package/dist/commands/workflow/instructions.js +219 -128
- package/dist/commands/workflow/instructions.js.map +1 -1
- package/dist/commands/workflow/new-change.d.ts +2 -5
- package/dist/commands/workflow/new-change.d.ts.map +1 -1
- package/dist/commands/workflow/new-change.js +69 -32
- package/dist/commands/workflow/new-change.js.map +1 -1
- package/dist/commands/workflow/schemas.d.ts +1 -5
- package/dist/commands/workflow/schemas.d.ts.map +1 -1
- package/dist/commands/workflow/schemas.js +6 -17
- package/dist/commands/workflow/schemas.js.map +1 -1
- package/dist/commands/workflow/shared.d.ts +37 -42
- package/dist/commands/workflow/shared.d.ts.map +1 -1
- package/dist/commands/workflow/shared.js +23 -54
- package/dist/commands/workflow/shared.js.map +1 -1
- package/dist/commands/workflow/status.d.ts +7 -17
- package/dist/commands/workflow/status.d.ts.map +1 -1
- package/dist/commands/workflow/status.js +57 -72
- package/dist/commands/workflow/status.js.map +1 -1
- package/dist/commands/workflow/templates.d.ts +8 -8
- package/dist/commands/workflow/templates.d.ts.map +1 -1
- package/dist/commands/workflow/templates.js +32 -46
- package/dist/commands/workflow/templates.js.map +1 -1
- package/dist/core/archive.d.ts +22 -31
- package/dist/core/archive.d.ts.map +1 -1
- package/dist/core/archive.js +296 -283
- 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 +54 -120
- package/dist/core/artifact-graph/instruction-loader.d.ts.map +1 -1
- package/dist/core/artifact-graph/instruction-loader.js +129 -111
- 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 +36 -81
- package/dist/core/artifact-graph/resolver.d.ts.map +1 -1
- package/dist/core/artifact-graph/resolver.js +60 -101
- 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 +6 -14
- package/dist/core/artifact-graph/stub-detection.d.ts.map +1 -1
- package/dist/core/artifact-graph/stub-detection.js +13 -16
- package/dist/core/artifact-graph/stub-detection.js.map +1 -1
- 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 +16 -27
- package/dist/core/change-presenter.d.ts.map +1 -1
- package/dist/core/change-presenter.js +53 -53
- package/dist/core/change-presenter.js.map +1 -1
- package/dist/core/change-status-policy.d.ts +4 -8
- package/dist/core/change-status-policy.d.ts.map +1 -1
- package/dist/core/change-status-policy.js +9 -17
- 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 +7 -41
- package/dist/core/config-schema.d.ts.map +1 -1
- package/dist/core/config-schema.js +35 -74
- package/dist/core/config-schema.js.map +1 -1
- package/dist/core/config.d.ts +25 -49
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +22 -45
- 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 +35 -22
- package/dist/core/dashboard-data.d.ts.map +1 -1
- package/dist/core/dashboard-data.js +57 -72
- 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 +12 -28
- package/dist/core/init.d.ts.map +1 -1
- package/dist/core/init.js +90 -168
- package/dist/core/init.js.map +1 -1
- package/dist/core/list.d.ts.map +1 -1
- package/dist/core/list.js +80 -38
- 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 +15 -23
- package/dist/core/markdown-render.d.ts.map +1 -1
- package/dist/core/markdown-render.js +25 -34
- package/dist/core/markdown-render.js.map +1 -1
- package/dist/core/migrate.d.ts +19 -16
- package/dist/core/migrate.d.ts.map +1 -1
- package/dist/core/migrate.js +151 -135
- 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 +23 -30
- 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 +8 -14
- package/dist/core/root-selection.d.ts.map +1 -1
- package/dist/core/root-selection.js +3 -6
- package/dist/core/root-selection.js.map +1 -1
- package/dist/core/rules.d.ts.map +1 -1
- package/dist/core/rules.js +2 -3
- 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.map +1 -1
- package/dist/core/schemas/base.schema.js +6 -12
- package/dist/core/schemas/base.schema.js.map +1 -1
- package/dist/core/schemas/change.schema.d.ts +8 -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/shared/index.d.ts +2 -7
- package/dist/core/shared/index.d.ts.map +1 -1
- package/dist/core/shared/index.js +2 -7
- package/dist/core/shared/index.js.map +1 -1
- package/dist/core/shared/rules-generation.d.ts +5 -15
- package/dist/core/shared/rules-generation.d.ts.map +1 -1
- package/dist/core/shared/rules-generation.js +33 -37
- 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 +35 -76
- package/dist/core/shared/tool-detection.d.ts.map +1 -1
- package/dist/core/shared/tool-detection.js +73 -93
- 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 +5 -10
- package/dist/core/spec-presenter.js.map +1 -1
- package/dist/core/specs-apply.d.ts +16 -31
- package/dist/core/specs-apply.d.ts.map +1 -1
- package/dist/core/specs-apply.js +146 -195
- 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 +16 -5
- package/dist/core/templates/workflows/archive.js.map +1 -1
- package/dist/core/templates/workflows/decision.js +3 -3
- 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 +10 -36
- package/dist/core/update.d.ts.map +1 -1
- package/dist/core/update.js +59 -126
- 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 -25
- 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 +30 -12
- 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 +42 -68
- package/dist/core/validation/validator.d.ts.map +1 -1
- package/dist/core/validation/validator.js +468 -285
- 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 +24 -76
- package/dist/utils/change-utils.d.ts.map +1 -1
- package/dist/utils/change-utils.js +95 -142
- 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 +10 -20
- package/dist/utils/item-discovery.d.ts.map +1 -1
- package/dist/utils/item-discovery.js +31 -55
- 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/issue/schema.yaml +8 -1
- package/schemas/issue/templates/spec.md +20 -3
- package/schemas/sdd/schema.yaml +20 -1
- package/schemas/sdd/templates/spec.md +20 -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,9 +55,21 @@ 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 = [];
|
|
77
75
|
const notice = (code, message) => {
|
|
@@ -79,36 +77,31 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
79
77
|
if (!options.silent)
|
|
80
78
|
console.log(`Warning: ${message}`);
|
|
81
79
|
};
|
|
82
|
-
// Read change spec content (delta-format expected)
|
|
83
80
|
const changeContent = await fs.readFile(update.source, 'utf-8');
|
|
84
|
-
// Parse deltas from the change spec file
|
|
85
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.
|
|
86
84
|
const specName = path.basename(path.dirname(update.target));
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (modifiedNames.has(name)) {
|
|
100
|
-
throw new Error(`${specName} validation failed - duplicate requirement in MODIFIED for header "### Requirement: ${mod.name}"`);
|
|
101
|
-
}
|
|
102
|
-
modifiedNames.add(name);
|
|
103
|
-
}
|
|
104
|
-
const removedNamesSet = new Set();
|
|
105
|
-
for (const rem of plan.removed) {
|
|
106
|
-
const name = normalizeRequirementName(rem);
|
|
107
|
-
if (removedNamesSet.has(name)) {
|
|
108
|
-
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);
|
|
109
97
|
}
|
|
110
|
-
|
|
111
|
-
}
|
|
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.
|
|
112
105
|
const renamedFromSet = new Set();
|
|
113
106
|
const renamedToSet = new Set();
|
|
114
107
|
for (const { from, to } of plan.renamed) {
|
|
@@ -123,7 +116,6 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
123
116
|
renamedFromSet.add(fromNorm);
|
|
124
117
|
renamedToSet.add(toNorm);
|
|
125
118
|
}
|
|
126
|
-
// Pre-validate cross-section conflicts
|
|
127
119
|
const conflicts = [];
|
|
128
120
|
for (const n of modifiedNames) {
|
|
129
121
|
if (removedNamesSet.has(n))
|
|
@@ -135,15 +127,12 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
135
127
|
if (removedNamesSet.has(n))
|
|
136
128
|
conflicts.push({ name: n, a: 'ADDED', b: 'REMOVED' });
|
|
137
129
|
}
|
|
138
|
-
// Renamed interplay: MODIFIED must reference the NEW header, not FROM
|
|
139
130
|
for (const { from, to } of plan.renamed) {
|
|
140
131
|
const fromNorm = normalizeRequirementName(from);
|
|
141
132
|
const toNorm = normalizeRequirementName(to);
|
|
142
|
-
// A REMOVED naming the FROM side contradicts the rename.
|
|
143
|
-
// REMOVED target is
|
|
144
|
-
//
|
|
145
|
-
// Compared folded, so a case/whitespace variant cannot slip past into a
|
|
146
|
-
// 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.
|
|
147
136
|
const removedFoldMatch = [...removedNamesSet].find((r) => foldRequirementName(r) === foldRequirementName(fromNorm));
|
|
148
137
|
if (removedFoldMatch !== undefined) {
|
|
149
138
|
throw new Error(`${specName} validation failed - requirement present in multiple sections (RENAMED and REMOVED) for header "### Requirement: ${from}"` +
|
|
@@ -152,7 +141,6 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
152
141
|
if (modifiedNames.has(fromNorm)) {
|
|
153
142
|
throw new Error(`${specName} validation failed - when a rename exists, MODIFIED must reference the NEW header "### Requirement: ${to}"`);
|
|
154
143
|
}
|
|
155
|
-
// Detect ADDED colliding with a RENAMED TO
|
|
156
144
|
if (addedNames.has(toNorm)) {
|
|
157
145
|
throw new Error(`${specName} validation failed - RENAMED TO header collides with ADDED for "### Requirement: ${to}"`);
|
|
158
146
|
}
|
|
@@ -166,33 +154,25 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
166
154
|
throw new Error(`Delta parsing found no operations for ${path.basename(path.dirname(update.source))}. ` +
|
|
167
155
|
`Provide ADDED/MODIFIED/REMOVED/RENAMED sections in change spec.`);
|
|
168
156
|
}
|
|
169
|
-
// Load or create base target content
|
|
170
157
|
let targetContent;
|
|
171
158
|
let isNewSpec = false;
|
|
172
159
|
try {
|
|
173
160
|
targetContent = await fs.readFile(update.target, 'utf-8');
|
|
174
161
|
}
|
|
175
162
|
catch (error) {
|
|
176
|
-
// Only "
|
|
177
|
-
//
|
|
178
|
-
// says the spec may well exist with content this path is about to discard.
|
|
179
|
-
// For an addition-only delta that is silent data loss: the new-capability
|
|
180
|
-
// 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.
|
|
181
165
|
if (!isMissingPathError(error))
|
|
182
166
|
throw error;
|
|
183
|
-
// Target spec does not exist; MODIFIED and RENAMED are not allowed for new specs
|
|
184
|
-
// REMOVED will be ignored with a warning since there's nothing to remove
|
|
185
167
|
if (plan.modified.length > 0 || plan.renamed.length > 0) {
|
|
186
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.`);
|
|
187
169
|
}
|
|
188
|
-
// Warn about REMOVED requirements being ignored for new specs
|
|
189
170
|
if (plan.removed.length > 0) {
|
|
190
171
|
notice('archive_removed_ignored_new_spec', `${specName} - ${plan.removed.length} REMOVED requirement(s) ignored for new spec (nothing to remove).`);
|
|
191
172
|
}
|
|
192
173
|
isNewSpec = true;
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
// 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.
|
|
196
176
|
const deltaPurpose = extractDeltaPurpose(changeContent);
|
|
197
177
|
if (deltaPurpose && isPurposeCarrySafe(deltaPurpose)) {
|
|
198
178
|
targetContent = buildSpecSkeleton(specName, changeName, deltaPurpose);
|
|
@@ -202,35 +182,27 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
202
182
|
notice('archive_delta_purpose_ignored', `${specName} - delta Purpose ignored (it would leave the new spec unreadable); wrote the placeholder Purpose instead.`);
|
|
203
183
|
}
|
|
204
184
|
else {
|
|
205
|
-
// The schema calls `## Purpose` mandatory for a new capability
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
// nobody told. Saying so here is the only moment the change that owns
|
|
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
|
|
209
188
|
// the answer is still in hand.
|
|
210
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.`);
|
|
211
190
|
}
|
|
212
191
|
targetContent = buildSpecSkeleton(specName, changeName);
|
|
213
192
|
}
|
|
214
193
|
}
|
|
215
|
-
// An existing capability's main spec owns its Purpose
|
|
216
|
-
// on
|
|
217
|
-
// The schema instruction tells authors not to write one here — which is
|
|
218
|
-
// exactly why someone who did needs to hear that it went nowhere, rather than
|
|
219
|
-
// assume the main spec now says what they wrote.
|
|
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.
|
|
220
196
|
if (!isNewSpec) {
|
|
221
197
|
const strayPurpose = extractDeltaPurpose(changeContent);
|
|
222
198
|
if (strayPurpose) {
|
|
223
|
-
notice('archive_delta_purpose_ignored', `${specName} - delta "## Purpose" ignored; ${
|
|
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.`);
|
|
224
200
|
}
|
|
225
201
|
}
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
// every structural regex below assumes LF — so the fix is to carry the
|
|
231
|
-
// convention forward, not to stop normalizing. A whole-file flag rather than
|
|
232
|
-
// per-line endings: a merged spec contains lines that had no original, so
|
|
233
|
-
// per-line fidelity is undefined for them.
|
|
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.
|
|
234
206
|
const crlf = targetContent.includes('\r\n');
|
|
235
207
|
const structureIssues = findMainSpecStructureIssues(targetContent);
|
|
236
208
|
if (structureIssues.length > 0) {
|
|
@@ -239,27 +211,22 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
239
211
|
.join('\n');
|
|
240
212
|
throw new Error(`${specName}: target spec is structurally invalid and cannot be updated until fixed:\n${details}`);
|
|
241
213
|
}
|
|
242
|
-
// Extract requirements section and build name->block map
|
|
243
214
|
const parts = extractRequirementsSection(targetContent);
|
|
244
215
|
const nameToBlock = new Map();
|
|
245
216
|
for (const block of parts.bodyBlocks) {
|
|
246
217
|
nameToBlock.set(normalizeRequirementName(block.name), block);
|
|
247
218
|
}
|
|
248
|
-
// The
|
|
249
|
-
// block's key, so `bodyBlocks` alone
|
|
250
|
-
//
|
|
251
|
-
// 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.
|
|
252
222
|
const orderedKeys = parts.bodyBlocks.map((block) => normalizeRequirementName(block.name));
|
|
253
|
-
//
|
|
254
|
-
// RENAMED
|
|
223
|
+
// Order matters: RENAMED → REMOVED → MODIFIED → ADDED.
|
|
255
224
|
let renamedApplied = 0;
|
|
256
225
|
for (const r of plan.renamed) {
|
|
257
226
|
const from = normalizeRequirementName(r.from);
|
|
258
227
|
const to = normalizeRequirementName(r.to);
|
|
259
228
|
if (!nameToBlock.has(from)) {
|
|
260
|
-
// Source gone but target present
|
|
261
|
-
// the baseline (early-sync pattern) - re-applying it is a no-op, not a
|
|
262
|
-
// failure. Only a missing source AND target is a genuine error.
|
|
229
|
+
// Source gone but target present: the rename was already synced.
|
|
263
230
|
if (nameToBlock.has(to)) {
|
|
264
231
|
continue;
|
|
265
232
|
}
|
|
@@ -280,72 +247,88 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
280
247
|
};
|
|
281
248
|
nameToBlock.delete(from);
|
|
282
249
|
nameToBlock.set(to, renamedBlock);
|
|
283
|
-
// delete+set moves the entry to the Map's insertion-order tail,
|
|
284
|
-
//
|
|
285
|
-
// 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.
|
|
286
252
|
const orderIndex = orderedKeys.indexOf(from);
|
|
287
253
|
if (orderIndex >= 0) {
|
|
288
254
|
orderedKeys[orderIndex] = to;
|
|
289
255
|
}
|
|
290
256
|
renamedApplied++;
|
|
291
257
|
}
|
|
292
|
-
// REMOVED
|
|
293
258
|
let removedApplied = 0;
|
|
294
259
|
// Keys this run actually removed. A renamed requirement also disappears from
|
|
295
|
-
// its old key, so "absent from nameToBlock"
|
|
296
|
-
//
|
|
260
|
+
// its old key, so "absent from nameToBlock" cannot tell the two apart, and
|
|
261
|
+
// salvaging a renamed block's tail would duplicate it.
|
|
297
262
|
const removedKeys = new Set();
|
|
298
263
|
for (const name of plan.removed) {
|
|
299
264
|
const key = normalizeRequirementName(name);
|
|
300
265
|
if (!nameToBlock.has(key)) {
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
// only in case or interior whitespace still being present. That is a typo,
|
|
305
|
-
// 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.
|
|
306
269
|
if (!isNewSpec) {
|
|
307
270
|
const nearMiss = [...nameToBlock.keys()].find((k) => foldRequirementName(k) === foldRequirementName(key));
|
|
308
271
|
if (nearMiss !== undefined) {
|
|
309
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`);
|
|
310
273
|
}
|
|
311
|
-
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/${
|
|
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.`);
|
|
312
275
|
}
|
|
313
|
-
// Skip removal (nothing to remove)
|
|
314
276
|
continue;
|
|
315
277
|
}
|
|
316
278
|
nameToBlock.delete(key);
|
|
317
279
|
removedKeys.add(key);
|
|
318
280
|
removedApplied++;
|
|
319
281
|
}
|
|
320
|
-
// 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);
|
|
321
289
|
for (const mod of plan.modified) {
|
|
322
290
|
const key = normalizeRequirementName(mod.name);
|
|
323
291
|
const currentBlock = nameToBlock.get(key);
|
|
324
292
|
if (!currentBlock) {
|
|
325
293
|
throw new Error(`${specName} MODIFIED failed for header "### Requirement: ${mod.name}" - not found`);
|
|
326
294
|
}
|
|
327
|
-
|
|
328
|
-
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);
|
|
329
296
|
if (!modHeaderMatch || normalizeRequirementName(modHeaderMatch[1]) !== key) {
|
|
330
297
|
throw new Error(`${specName} MODIFIED failed for header "### Requirement: ${mod.name}" - header mismatch in content`);
|
|
331
298
|
}
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
|
|
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
|
+
});
|
|
335
312
|
if (missingScenarios.length > 0) {
|
|
336
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.`);
|
|
337
314
|
}
|
|
338
|
-
|
|
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.`);
|
|
339
324
|
}
|
|
340
|
-
// ADDED
|
|
341
325
|
let addedApplied = 0;
|
|
342
326
|
for (const add of plan.added) {
|
|
343
327
|
const key = normalizeRequirementName(add.name);
|
|
344
328
|
const existing = nameToBlock.get(key);
|
|
345
329
|
if (existing) {
|
|
346
|
-
// Identical content means
|
|
347
|
-
//
|
|
348
|
-
// conflict. Only differing content is a genuine collision.
|
|
330
|
+
// Identical content means it was already synced; only differing content
|
|
331
|
+
// is a genuine collision.
|
|
349
332
|
if (normalizeBlockRaw(existing.raw) === normalizeBlockRaw(add.raw)) {
|
|
350
333
|
continue;
|
|
351
334
|
}
|
|
@@ -354,13 +337,10 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
354
337
|
nameToBlock.set(key, add);
|
|
355
338
|
addedApplied++;
|
|
356
339
|
}
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
// it. Removing that requirement used to delete the absorbed content with it,
|
|
362
|
-
// silently: nothing counted it, so nothing warned, and the spec left behind
|
|
363
|
-
// 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.
|
|
364
344
|
const pieces = [];
|
|
365
345
|
const seen = new Set();
|
|
366
346
|
let salvagedCount = 0;
|
|
@@ -384,7 +364,7 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
384
364
|
}
|
|
385
365
|
}
|
|
386
366
|
}
|
|
387
|
-
//
|
|
367
|
+
// Whatever was added rather than replacing an original slot.
|
|
388
368
|
for (const [key, block] of nameToBlock.entries()) {
|
|
389
369
|
if (!seen.has(key)) {
|
|
390
370
|
pieces.push(block.raw);
|
|
@@ -392,8 +372,7 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
392
372
|
}
|
|
393
373
|
}
|
|
394
374
|
// Every requirement is gone but absorbed content survived. Retiring would
|
|
395
|
-
// delete that content
|
|
396
|
-
// 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
|
|
397
376
|
// requirements, which cannot validate. Neither is safe to choose silently.
|
|
398
377
|
if (keptRequirements === 0 && removedApplied > 0 && salvagedCount > 0) {
|
|
399
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.`);
|
|
@@ -403,26 +382,19 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
403
382
|
.concat(pieces)
|
|
404
383
|
.join('\n\n')
|
|
405
384
|
.trimEnd();
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
// the heading to the Purpose paragraph above it and the first requirement
|
|
410
|
-
// below, so every archive rewrote a well-formatted spec into that shape.
|
|
411
|
-
// Separate non-empty slices with one blank line, and end the file with
|
|
412
|
-
// 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.
|
|
413
388
|
const rebuilt = collapseBlankRunsOutsideFences([parts.before.trimEnd(), parts.headerLine, reqBody, parts.after.trim()]
|
|
414
389
|
.filter((s) => s !== '')
|
|
415
390
|
.join('\n\n')).trimEnd() + '\n';
|
|
416
391
|
return {
|
|
417
392
|
rebuilt,
|
|
418
393
|
crlf,
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
// requirements. An emptied spec cannot be written either way — it fails
|
|
424
|
-
// validation ("Spec must have at least one requirement") — so the caller
|
|
425
|
-
// 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.
|
|
426
398
|
retired: keptRequirements === 0 && removedApplied > 0,
|
|
427
399
|
notices,
|
|
428
400
|
counts: {
|
|
@@ -434,18 +406,13 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
434
406
|
};
|
|
435
407
|
}
|
|
436
408
|
function normalizeBlockRaw(raw) {
|
|
437
|
-
return raw
|
|
409
|
+
return normalizeDocument(raw).trim();
|
|
438
410
|
}
|
|
439
411
|
/**
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
* to the whole document as `replace(/\n{3,}/g, '\n\n')` it could not tell a seam
|
|
445
|
-
* from a blank line an author put inside a code sample deliberately, so every
|
|
446
|
-
* archive quietly edited the examples in a spec. The seams are all outside any
|
|
447
|
-
* fence by construction, so masking fenced lines loses nothing the collapse was
|
|
448
|
-
* 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.
|
|
449
416
|
*/
|
|
450
417
|
function collapseBlankRunsOutsideFences(content) {
|
|
451
418
|
const lines = content.split('\n');
|
|
@@ -455,9 +422,8 @@ function collapseBlankRunsOutsideFences(content) {
|
|
|
455
422
|
for (let i = 0; i < lines.length; i++) {
|
|
456
423
|
if (!mask[i] && lines[i].trim() === '') {
|
|
457
424
|
const last = kept.length - 1;
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
// 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.
|
|
461
427
|
if (last >= 0 && !keptMask[last] && kept[last].trim() === '')
|
|
462
428
|
continue;
|
|
463
429
|
}
|
|
@@ -466,16 +432,11 @@ function collapseBlankRunsOutsideFences(content) {
|
|
|
466
432
|
}
|
|
467
433
|
return kept.join('\n');
|
|
468
434
|
}
|
|
469
|
-
/**
|
|
470
|
-
* Write an updated spec to disk.
|
|
471
|
-
*/
|
|
472
435
|
export async function writeUpdatedSpec(update, rebuilt, counts, options = {}) {
|
|
473
|
-
// Create target directory if needed
|
|
474
436
|
const targetDir = path.dirname(update.target);
|
|
475
437
|
await fs.mkdir(targetDir, { recursive: true });
|
|
476
|
-
// `rebuilt` is pure LF by construction; restore the
|
|
477
|
-
//
|
|
478
|
-
// 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.
|
|
479
440
|
const content = options.crlf ? rebuilt.replace(/\r?\n/g, '\r\n') : rebuilt;
|
|
480
441
|
await fs.writeFile(update.target, content);
|
|
481
442
|
if (options.silent)
|
|
@@ -493,19 +454,15 @@ export async function writeUpdatedSpec(update, rebuilt, counts, options = {}) {
|
|
|
493
454
|
}
|
|
494
455
|
/**
|
|
495
456
|
* The tail of a requirement block that is not part of the requirement: content
|
|
496
|
-
* from the first `#`/`##`/`###` heading after
|
|
497
|
-
*
|
|
498
|
-
* `####` is excluded deliberately — a requirement's `#### Scenario:` headings
|
|
499
|
-
* 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.
|
|
500
459
|
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
* a spec that was valid becomes invalid. Salvaging on removal changes what a
|
|
505
|
-
* 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.
|
|
506
463
|
*/
|
|
507
464
|
function salvageAbsorbedContent(requirementRaw) {
|
|
508
|
-
const lines = requirementRaw
|
|
465
|
+
const lines = normalizeDocument(requirementRaw).split('\n');
|
|
509
466
|
const mask = buildCodeFenceMask(lines);
|
|
510
467
|
for (let i = 1; i < lines.length; i++) {
|
|
511
468
|
if (mask[i])
|
|
@@ -517,12 +474,9 @@ function salvageAbsorbedContent(requirementRaw) {
|
|
|
517
474
|
return '';
|
|
518
475
|
}
|
|
519
476
|
/**
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
* The pruning walks up from the spec file and stops at `specsRoot`, which is
|
|
524
|
-
* never removed even if it ends up empty — a project with no capabilities still
|
|
525
|
-
* 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
|
|
526
480
|
* sibling capability is never touched.
|
|
527
481
|
*/
|
|
528
482
|
export async function retireSpec(update, specsRoot, options = {}) {
|
|
@@ -547,9 +501,8 @@ export async function retireSpec(update, specsRoot, options = {}) {
|
|
|
547
501
|
console.log(`Retiring ${options.displayPath ?? `tospec/specs/${specName}/spec.md`}: no requirements left`);
|
|
548
502
|
}
|
|
549
503
|
/**
|
|
550
|
-
*
|
|
551
|
-
*
|
|
552
|
-
* 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.
|
|
553
506
|
*/
|
|
554
507
|
export function buildSpecSkeleton(specFolderName, changeName, purpose) {
|
|
555
508
|
const titleBase = specFolderName;
|
|
@@ -563,8 +516,10 @@ export function buildSpecSkeleton(specFolderName, changeName, purpose) {
|
|
|
563
516
|
* is none. Fence-masked so a `## Purpose` inside a code example is not mistaken
|
|
564
517
|
* for the real section.
|
|
565
518
|
*/
|
|
566
|
-
function extractDeltaPurpose(content) {
|
|
567
|
-
|
|
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');
|
|
568
523
|
const mask = buildCodeFenceMask(lines);
|
|
569
524
|
let start = -1;
|
|
570
525
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -589,13 +544,9 @@ function extractDeltaPurpose(content) {
|
|
|
589
544
|
return lines.slice(start, end).join('\n').trim();
|
|
590
545
|
}
|
|
591
546
|
/**
|
|
592
|
-
* A carried Purpose must not restructure the new spec
|
|
593
|
-
* and code fences each
|
|
594
|
-
*
|
|
595
|
-
* it.
|
|
596
|
-
* Limitation: prose-only heuristic; a Purpose that genuinely needs a fence or
|
|
597
|
-
* heading falls back to TBD. Loosen only once those corruption classes are
|
|
598
|
-
* 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.
|
|
599
550
|
*/
|
|
600
551
|
function isPurposeCarrySafe(body) {
|
|
601
552
|
if (!body)
|