@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
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Decision
|
|
2
|
+
* `tospec decision` — Architecture Decision Records. Unlike the `change`
|
|
3
|
+
* workflow these are permanent: one
|
|
4
|
+
* `tospec/decisions/<yyyyMMdd_HHmmss>-<topic>.md` file each.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `tospec/decisions
|
|
7
|
-
*
|
|
8
|
-
* `decision new` produces both files from the CLI's own templates: it writes the
|
|
9
|
-
* dated decision file from `decision.md` (title / status / date pre-filled) and
|
|
10
|
-
* appends a row to the persisted ledger `tospec/decisions/index.md`
|
|
11
|
-
* (timestamp / title / summary), seeding it from `index.md` on first use. The
|
|
12
|
-
* agent then fills the decision's prose sections.
|
|
13
|
-
*
|
|
14
|
-
* `decision list` derives a live status view from the files on disk.
|
|
15
|
-
*
|
|
16
|
-
* The `decision` schema (schemas/decision/) supplies both templates and the
|
|
17
|
-
* required-sections contract; this command consumes them.
|
|
6
|
+
* `decision new` writes that file from the `decision` schema's template (title /
|
|
7
|
+
* status / date pre-filled) and appends a row to the ledger
|
|
8
|
+
* `tospec/decisions/index.md`, seeded from `index.md` on first use; the agent
|
|
9
|
+
* fills the prose sections. `decision list` derives a live view from disk.
|
|
18
10
|
*/
|
|
19
11
|
import { Option } from 'commander';
|
|
20
12
|
import path from 'path';
|
|
@@ -22,27 +14,36 @@ import * as fs from 'fs';
|
|
|
22
14
|
import { resolveRootForCommand, toRootOutput, } from '../core/root-selection.js';
|
|
23
15
|
import { loadTemplate } from '../core/artifact-graph/index.js';
|
|
24
16
|
import { MarkdownParser, findSection } from '../core/parsers/markdown-parser.js';
|
|
25
|
-
import { validateChangeName } from '../utils/change-utils.js';
|
|
17
|
+
import { completeRootStructure, validateChangeName } from '../utils/change-utils.js';
|
|
26
18
|
import { formatTimestamp } from '../utils/timestamp.js';
|
|
19
|
+
import { DEFAULT_SCHEMA, DECISION_SCHEMA_NAME } from '../core/schema-names.js';
|
|
27
20
|
import { emitSuccess, emitFailure } from './shared-output.js';
|
|
28
|
-
|
|
29
|
-
// Types
|
|
30
|
-
// -----------------------------------------------------------------------------
|
|
21
|
+
import { stripBom } from '../core/parsers/requirement-text.js';
|
|
31
22
|
const DECISION_STATUSES = ['proposed', 'accepted', 'superseded'];
|
|
32
|
-
|
|
23
|
+
// Re-exported for callers that reached it here; defined in core/schema-names so
|
|
24
|
+
// workflow/shared.ts no longer has to import this command module.
|
|
25
|
+
export { DECISION_SCHEMA_NAME };
|
|
26
|
+
/** The `--json` null-shapes; shared by the commander table and the catch sites. */
|
|
27
|
+
export const DECISION_NEW_FAILURE_PAYLOAD = {
|
|
28
|
+
decision: null,
|
|
29
|
+
root: null,
|
|
30
|
+
};
|
|
31
|
+
export const DECISION_LIST_FAILURE_PAYLOAD = {
|
|
32
|
+
decisions: null,
|
|
33
|
+
root: null,
|
|
34
|
+
};
|
|
35
|
+
const SCHEMA_NAME = DECISION_SCHEMA_NAME;
|
|
33
36
|
const DECISIONS_SUBDIR = path.join('tospec', 'decisions');
|
|
34
37
|
// Aligns with the project-wide CLI timestamp (formatTimestamp): yyyyMMdd_HHmmss.
|
|
35
38
|
const TIMESTAMP_RE = /^\d{8}_\d{6}$/;
|
|
39
|
+
const TIMESTAMP_PARTS_RE = /^(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})$/;
|
|
36
40
|
const FILE_RE = /^(\d{8}_\d{6})-(.+)\.md$/;
|
|
37
|
-
// -----------------------------------------------------------------------------
|
|
38
|
-
// Helpers
|
|
39
|
-
// -----------------------------------------------------------------------------
|
|
40
41
|
/** Title = the H1 heading; falls back to the topic derived from the filename. */
|
|
41
42
|
function parseTitle(sections, fallback) {
|
|
42
43
|
const h1 = sections.find((s) => s.level === 1);
|
|
43
44
|
return h1?.title.trim() || fallback;
|
|
44
45
|
}
|
|
45
|
-
/** Status = the first ASCII-word bullet under
|
|
46
|
+
/** Status = the first ASCII-word bullet under `Status` (the `Date:` bullet follows it). */
|
|
46
47
|
function parseStatus(sections) {
|
|
47
48
|
const statusSection = findSection(sections, 'Status');
|
|
48
49
|
if (!statusSection)
|
|
@@ -85,9 +86,29 @@ function renderDecision(template, title, stamp, status) {
|
|
|
85
86
|
.replace('[yyyyMMdd_HHmmss]', stamp)
|
|
86
87
|
.replace(/^- proposed$/m, `- ${status}`);
|
|
87
88
|
}
|
|
88
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* Whether a shape-valid stamp also names a real instant.
|
|
91
|
+
*
|
|
92
|
+
* `TIMESTAMP_RE` alone accepts `20261345_996199`, and that value becomes the
|
|
93
|
+
* filename, the ledger's date and the `--sort date` key. Round-tripped rather
|
|
94
|
+
* than range-checked per field, so month lengths and leap days come from the
|
|
95
|
+
* calendar.
|
|
96
|
+
*/
|
|
97
|
+
function isRealTimestamp(stamp) {
|
|
98
|
+
const m = TIMESTAMP_PARTS_RE.exec(stamp);
|
|
99
|
+
if (!m)
|
|
100
|
+
return false;
|
|
101
|
+
const [, y, mo, d, h, mi, sec] = m.map(Number);
|
|
102
|
+
// UTC, not local time: the stamp is a calendar label, and a local round-trip
|
|
103
|
+
// rejected the hour a DST transition skips (02:30 on the spring-forward day
|
|
104
|
+
// in a DST zone), making a `--date` valid on one machine and not another.
|
|
105
|
+
const date = new Date(Date.UTC(y, mo - 1, d, h, mi, sec));
|
|
106
|
+
return (date.getUTCFullYear() === y && date.getUTCMonth() === mo - 1 && date.getUTCDate() === d
|
|
107
|
+
&& date.getUTCHours() === h && date.getUTCMinutes() === mi && date.getUTCSeconds() === sec);
|
|
108
|
+
}
|
|
109
|
+
/** `20260722_161825` -> `2026-07-22 16:18:25`, or the raw stamp if unparseable. */
|
|
89
110
|
function stampToHuman(stamp) {
|
|
90
|
-
const m =
|
|
111
|
+
const m = TIMESTAMP_PARTS_RE.exec(stamp);
|
|
91
112
|
if (!m)
|
|
92
113
|
return stamp;
|
|
93
114
|
const [, y, mo, d, h, mi, s] = m;
|
|
@@ -107,22 +128,246 @@ function ensureIndex(decisionsDir, projectRoot) {
|
|
|
107
128
|
}
|
|
108
129
|
return indexPath;
|
|
109
130
|
}
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
function appendIndexRow(indexPath, timestamp, title, summary, file) {
|
|
131
|
+
// `file` is the sibling filename (index.md lives in the decisions dir), so the
|
|
132
|
+
// relative link resolves straight to the decision doc.
|
|
133
|
+
// Limitation: no dedup by topic — two dates for one topic are two rows.
|
|
134
|
+
function indexRow(timestamp, status, title, summary, file) {
|
|
115
135
|
const fileLink = `[${indexCell(file)}](${file})`;
|
|
116
|
-
|
|
136
|
+
return `| ${timestamp} | ${indexCell(status)} | ${indexCell(title)} | ${indexCell(summary)} | ${fileLink} |`;
|
|
137
|
+
}
|
|
138
|
+
/** The header a current ledger carries; `status` is the column added below. */
|
|
139
|
+
const INDEX_HEADER = '| timestamp | status | title | summary | file |';
|
|
140
|
+
const INDEX_SEPARATOR = '| ----- | ----- | ----- | ----- | ----- |';
|
|
141
|
+
/**
|
|
142
|
+
* Brings a pre-status ledger up to the current five-column shape. Without the
|
|
143
|
+
* column a superseded decision is indistinguishable from a live one.
|
|
144
|
+
*
|
|
145
|
+
* Each row's status is read from the file its link points at rather than left
|
|
146
|
+
* blank; a missing file keeps the `unknown` `decision list` reports. A column
|
|
147
|
+
* insert, not a rebuild, so hand-written cells survive.
|
|
148
|
+
*/
|
|
149
|
+
function upgradeIndexColumns(lines, decisionsDir) {
|
|
150
|
+
return lines.map((line) => {
|
|
151
|
+
const trimmed = line.trim();
|
|
152
|
+
if (!trimmed.startsWith('|'))
|
|
153
|
+
return line;
|
|
154
|
+
if (/^\|\s*timestamp\s*\|/i.test(trimmed))
|
|
155
|
+
return INDEX_HEADER;
|
|
156
|
+
if (/^[|\s-]+$/.test(trimmed))
|
|
157
|
+
return INDEX_SEPARATOR;
|
|
158
|
+
const cells = trimmed.slice(1, -1).split('|');
|
|
159
|
+
if (cells.length !== 4)
|
|
160
|
+
return line;
|
|
161
|
+
const fileMatch = /\]\(([^)]+)\)/.exec(cells[3]);
|
|
162
|
+
const status = fileMatch ? statusOfDecisionFile(decisionsDir, fileMatch[1]) : 'unknown';
|
|
163
|
+
return `|${cells[0]}| ${status} |${cells[1]}|${cells[2]}|${cells[3]}|`;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Re-reads every row's status from the decision file it links to, on every index
|
|
168
|
+
* write. A status changes on a *different* row than the one being written: the
|
|
169
|
+
* decision skill tells the author to flip an older record to `superseded` and
|
|
170
|
+
* forbids hand-editing `index.md`.
|
|
171
|
+
*
|
|
172
|
+
* Only the status cell is touched; hand-edited cells survive, and so does a row
|
|
173
|
+
* whose file is gone — see the guard below.
|
|
174
|
+
*/
|
|
175
|
+
function refreshIndexStatuses(lines, decisionsDir) {
|
|
176
|
+
return lines.map((line) => {
|
|
177
|
+
const trimmed = line.trim();
|
|
178
|
+
if (!trimmed.startsWith('|'))
|
|
179
|
+
return line;
|
|
180
|
+
if (/^\|\s*timestamp\s*\|/i.test(trimmed))
|
|
181
|
+
return line;
|
|
182
|
+
if (/^[|\s-]+$/.test(trimmed))
|
|
183
|
+
return line;
|
|
184
|
+
const cells = trimmed.slice(1, -1).split('|');
|
|
185
|
+
if (cells.length !== 5)
|
|
186
|
+
return line;
|
|
187
|
+
const fileMatch = /\]\(([^)]+)\)/.exec(cells[4]);
|
|
188
|
+
if (!fileMatch)
|
|
189
|
+
return line;
|
|
190
|
+
// A row whose file no longer exists is left exactly as it is: rewriting it
|
|
191
|
+
// to `unknown` would overwrite the last surviving record of that decision,
|
|
192
|
+
// and would count as a refresh on a row that never drifted.
|
|
193
|
+
if (!fs.existsSync(path.join(decisionsDir, fileMatch[1])))
|
|
194
|
+
return line;
|
|
195
|
+
const status = statusOfDecisionFile(decisionsDir, fileMatch[1]);
|
|
196
|
+
return `|${cells[0]}| ${status} |${cells[2]}|${cells[3]}|${cells[4]}|`;
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
/** The status recorded in a decision file, or `unknown` when it cannot be read. */
|
|
200
|
+
function statusOfDecisionFile(decisionsDir, file) {
|
|
201
|
+
try {
|
|
202
|
+
const content = fs.readFileSync(path.join(decisionsDir, file), 'utf-8');
|
|
203
|
+
return parseStatus(new MarkdownParser(content).getSections());
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return 'unknown';
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Writes this decision's row, replacing the existing one for the same file: one
|
|
211
|
+
* row per file, so `--force` must neither append a second nor leave the old one
|
|
212
|
+
* advertising a title and status the file no longer has.
|
|
213
|
+
*
|
|
214
|
+
* Matched on the filename, not the title — the title is what a rewrite changes.
|
|
215
|
+
*/
|
|
216
|
+
function writeIndexRow(indexPath, timestamp, status, title, summary, file) {
|
|
217
|
+
const row = indexRow(timestamp, status, title, summary, file);
|
|
218
|
+
// BOM stripped so the rewritten ledger comes out BOM-free and the
|
|
219
|
+
// `| status |` header test below cannot be defeated by one.
|
|
220
|
+
const existing = stripBom(fs.readFileSync(indexPath, 'utf-8'));
|
|
221
|
+
// Preserve whatever endings the ledger already uses.
|
|
222
|
+
const eol = existing.includes('\r\n') ? '\r\n' : '\n';
|
|
223
|
+
let lines = existing.split(/\r?\n/);
|
|
224
|
+
// A ledger written before the status column gets it here, rather than growing
|
|
225
|
+
// a row one cell wider than its header.
|
|
226
|
+
if (!existing.includes('| status |')) {
|
|
227
|
+
lines = upgradeIndexColumns(lines, path.dirname(indexPath));
|
|
228
|
+
}
|
|
229
|
+
// Every other row's status is brought up to date in the same write. The row
|
|
230
|
+
// being written is excluded by construction — it is replaced with `row` below.
|
|
231
|
+
lines = refreshIndexStatuses(lines, path.dirname(indexPath));
|
|
232
|
+
const linkTarget = `](${file})`;
|
|
233
|
+
const index = lines.findIndex((line) => line.startsWith('|') && line.includes(linkTarget));
|
|
234
|
+
if (index === -1) {
|
|
235
|
+
const body = lines.join(eol);
|
|
236
|
+
const separator = body.length > 0 && !body.endsWith('\n') ? eol : '';
|
|
237
|
+
fs.writeFileSync(indexPath, body + separator + row + eol, 'utf-8');
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
lines[index] = row;
|
|
241
|
+
fs.writeFileSync(indexPath, lines.join(eol), 'utf-8');
|
|
242
|
+
}
|
|
243
|
+
/** The decision file an index row links to, or null for a non-row line. */
|
|
244
|
+
function indexRowFile(line) {
|
|
245
|
+
const trimmed = line.trim();
|
|
246
|
+
if (!trimmed.startsWith('|'))
|
|
247
|
+
return null;
|
|
248
|
+
return /\]\(([^)]+)\)/.exec(trimmed)?.[1] ?? null;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* A data row's timestamp cell, or null for the header, the separator and prose.
|
|
252
|
+
* `yyyy-MM-dd HH:mm:ss` sorts lexicographically in chronological order, so the
|
|
253
|
+
* string is its own comparison key — no date parsing, and a hand-edited cell
|
|
254
|
+
* compares as text rather than throwing.
|
|
255
|
+
*/
|
|
256
|
+
function indexRowTimestamp(line) {
|
|
257
|
+
const trimmed = line.trim();
|
|
258
|
+
if (!trimmed.startsWith('|'))
|
|
259
|
+
return null;
|
|
260
|
+
if (/^\|\s*timestamp\s*\|/i.test(trimmed))
|
|
261
|
+
return null;
|
|
262
|
+
if (/^[|\s-]+$/.test(trimmed))
|
|
263
|
+
return null;
|
|
264
|
+
const cells = trimmed.slice(1, -1).split('|');
|
|
265
|
+
if (cells.length !== 5)
|
|
266
|
+
return null;
|
|
267
|
+
return cells[0].trim();
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Splices recovered rows into the ledger in timestamp order rather than onto the
|
|
271
|
+
* end: a row recovered by `--reindex` is dated whenever it was actually decided,
|
|
272
|
+
* so appending left index.md disagreeing with the sorted `decision list`.
|
|
273
|
+
*
|
|
274
|
+
* Existing rows are never reordered, only inserted between, so a hand-arranged
|
|
275
|
+
* ledger keeps its arrangement.
|
|
276
|
+
*/
|
|
277
|
+
function spliceRowsByTimestamp(body, additions) {
|
|
278
|
+
const result = [...body];
|
|
279
|
+
for (const addition of additions) {
|
|
280
|
+
const at = result.findIndex((line) => {
|
|
281
|
+
const stamp = indexRowTimestamp(line);
|
|
282
|
+
return stamp !== null && stamp > addition.timestamp;
|
|
283
|
+
});
|
|
284
|
+
if (at === -1)
|
|
285
|
+
result.push(addition.row);
|
|
286
|
+
else
|
|
287
|
+
result.splice(at, 0, addition.row);
|
|
288
|
+
}
|
|
289
|
+
return result;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Rows whose linked decision file is gone, read without touching the ledger.
|
|
293
|
+
*
|
|
294
|
+
* Split out of `reindexDecisionIndex` so a plain `decision list` can report the
|
|
295
|
+
* same rot: `list` reads the decision files, so a row naming a deleted one is
|
|
296
|
+
* invisible to it, and the rot was only surfaced by `--reindex` — a flag whose
|
|
297
|
+
* name is about writing, which nobody reaches for to ask a question.
|
|
298
|
+
*/
|
|
299
|
+
export function findDanglingIndexRows(decisionsDir) {
|
|
300
|
+
const indexPath = path.join(decisionsDir, 'index.md');
|
|
301
|
+
if (!fs.existsSync(indexPath))
|
|
302
|
+
return [];
|
|
303
|
+
const lines = stripBom(fs.readFileSync(indexPath, 'utf-8')).split(/\r?\n/);
|
|
304
|
+
const linked = new Set(lines.map((line) => indexRowFile(line)).filter((file) => file !== null));
|
|
305
|
+
return [...linked].filter((file) => !fs.existsSync(path.join(decisionsDir, file)));
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Brings `index.md` back in line with the decision files without creating one.
|
|
309
|
+
* Three ways a ledger falls behind:
|
|
310
|
+
*
|
|
311
|
+
* 1. A stale status cell. Rewritten from the file it links to.
|
|
312
|
+
* 2. A decision file with no row (how one arrives from another branch).
|
|
313
|
+
* Inserted in timestamp order.
|
|
314
|
+
* 3. A row whose file is gone. Named in the result, never removed — the ledger
|
|
315
|
+
* is a historical record.
|
|
316
|
+
*/
|
|
317
|
+
export function reindexDecisionIndex(decisionsDir) {
|
|
318
|
+
const empty = { statusesRefreshed: 0, rowsAdded: [], danglingRows: [] };
|
|
319
|
+
const indexPath = path.join(decisionsDir, 'index.md');
|
|
320
|
+
if (!fs.existsSync(indexPath))
|
|
321
|
+
return empty;
|
|
322
|
+
const existing = stripBom(fs.readFileSync(indexPath, 'utf-8'));
|
|
323
|
+
const eol = existing.includes('\r\n') ? '\r\n' : '\n';
|
|
324
|
+
const lines = existing.split(/\r?\n/);
|
|
325
|
+
const refreshed = refreshIndexStatuses(lines, decisionsDir);
|
|
326
|
+
const statusesRefreshed = refreshed.reduce((count, line, i) => (line === lines[i] ? count : count + 1), 0);
|
|
327
|
+
const linked = new Set(refreshed.map((line) => indexRowFile(line)).filter((file) => file !== null));
|
|
328
|
+
const danglingRows = [...linked].filter((file) => !fs.existsSync(path.join(decisionsDir, file)));
|
|
329
|
+
// Oldest first, so recovered rows land in the order the ledger would have
|
|
330
|
+
// grown had they never gone missing.
|
|
331
|
+
const missing = readDecisionEntries(decisionsDir)
|
|
332
|
+
.filter((entry) => !linked.has(entry.file))
|
|
333
|
+
.sort((a, b) => a.date.localeCompare(b.date));
|
|
334
|
+
const added = missing.map((entry) => ({
|
|
335
|
+
timestamp: stampToHuman(entry.date),
|
|
336
|
+
// Summary left empty: no file carries it, and reusing the title would put
|
|
337
|
+
// text in the ledger nobody wrote.
|
|
338
|
+
row: indexRow(stampToHuman(entry.date), entry.status, entry.title, '', entry.file),
|
|
339
|
+
}));
|
|
340
|
+
if (statusesRefreshed > 0 || added.length > 0) {
|
|
341
|
+
const body = [...refreshed];
|
|
342
|
+
// Trailing blank lines are the file's ending, not rows; splice above them.
|
|
343
|
+
while (body.length > 0 && body[body.length - 1].trim() === '')
|
|
344
|
+
body.pop();
|
|
345
|
+
fs.writeFileSync(indexPath, spliceRowsByTimestamp(body, added).join(eol) + eol, 'utf-8');
|
|
346
|
+
}
|
|
347
|
+
return { statusesRefreshed, rowsAdded: missing.map((entry) => entry.file), danglingRows };
|
|
117
348
|
}
|
|
118
|
-
// -----------------------------------------------------------------------------
|
|
119
|
-
// Command implementations (exported for tests)
|
|
120
|
-
// -----------------------------------------------------------------------------
|
|
121
349
|
export async function decisionNewCommand(topic, options) {
|
|
350
|
+
// Root first, as `new change` does: an argument failure still happened
|
|
351
|
+
// somewhere, and `root: null` could not say where.
|
|
352
|
+
let resolvedRoot = null;
|
|
353
|
+
const failurePayload = () => ({
|
|
354
|
+
decision: null,
|
|
355
|
+
root: resolvedRoot ? toRootOutput(resolvedRoot) : null,
|
|
356
|
+
});
|
|
122
357
|
try {
|
|
358
|
+
// Same rule as `new change`: a writing command never scaffolds a root in
|
|
359
|
+
// an uninitialised directory; that is `tospec init`'s job.
|
|
360
|
+
resolvedRoot = await resolveRootForCommand(options, {
|
|
361
|
+
json: options.json,
|
|
362
|
+
allowImplicitRoot: false,
|
|
363
|
+
failurePayload: failurePayload(),
|
|
364
|
+
});
|
|
365
|
+
if (!resolvedRoot)
|
|
366
|
+
return;
|
|
367
|
+
const root = resolvedRoot;
|
|
123
368
|
if (!topic)
|
|
124
369
|
throw new Error('Missing required argument <topic>');
|
|
125
|
-
const nameValidation = validateChangeName(topic);
|
|
370
|
+
const nameValidation = validateChangeName(topic, 'Topic');
|
|
126
371
|
if (!nameValidation.valid) {
|
|
127
372
|
throw new Error(`Invalid topic '${topic}': ${nameValidation.error}`);
|
|
128
373
|
}
|
|
@@ -134,28 +379,43 @@ export async function decisionNewCommand(topic, options) {
|
|
|
134
379
|
if (!TIMESTAMP_RE.test(stamp)) {
|
|
135
380
|
throw new Error(`Invalid --date '${stamp}'. Expected project timestamp format: yyyyMMdd_HHmmss`);
|
|
136
381
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
});
|
|
141
|
-
if (!root)
|
|
142
|
-
return;
|
|
382
|
+
if (!isRealTimestamp(stamp)) {
|
|
383
|
+
throw new Error(`Invalid --date '${stamp}'. That is not a real date and time.`);
|
|
384
|
+
}
|
|
143
385
|
const decisionsDir = path.join(root.path, DECISIONS_SUBDIR);
|
|
144
386
|
const fileName = `${stamp}-${topic}.md`;
|
|
145
387
|
const filePath = path.join(decisionsDir, fileName);
|
|
146
|
-
if (fs.existsSync(filePath) && !options.force) {
|
|
147
|
-
throw new Error(`Decision already exists: ${filePath} (use --force to overwrite)`);
|
|
148
|
-
}
|
|
149
388
|
const title = options.title ?? topic;
|
|
150
389
|
const summary = options.summary ?? title;
|
|
151
390
|
const template = loadTemplate(SCHEMA_NAME, 'decision.md', root.path);
|
|
152
391
|
const content = renderDecision(template, title, stamp, status);
|
|
153
|
-
//
|
|
154
|
-
//
|
|
392
|
+
// The whole root, not just `decisions/`. The root above is always an
|
|
393
|
+
// explicit `tospec/` (allowImplicitRoot: false), but it may be a partial
|
|
394
|
+
// one — a `tospec/` with only `changes/` — and creating just `decisions/`
|
|
395
|
+
// inside it would leave a half-root that every other command resolves as
|
|
396
|
+
// a project. Directories only; no config.yaml is written here.
|
|
397
|
+
await completeRootStructure(root.path, DEFAULT_SCHEMA);
|
|
155
398
|
fs.mkdirSync(decisionsDir, { recursive: true });
|
|
156
|
-
|
|
399
|
+
if (options.force) {
|
|
400
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
// 'wx' makes the create atomic, closing the check-then-write window a
|
|
404
|
+
// separate existsSync would leave open.
|
|
405
|
+
try {
|
|
406
|
+
fs.writeFileSync(filePath, content, { encoding: 'utf-8', flag: 'wx' });
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
if (error.code === 'EEXIST') {
|
|
410
|
+
throw new Error(`Decision already exists: ${filePath} (use --force to overwrite)`);
|
|
411
|
+
}
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
157
415
|
const indexPath = ensureIndex(decisionsDir, root.path);
|
|
158
|
-
|
|
416
|
+
// Unconditional: a --force that rewrote the file has to carry its new title
|
|
417
|
+
// and summary into the ledger.
|
|
418
|
+
writeIndexRow(indexPath, stampToHuman(stamp), status, title, summary, fileName);
|
|
159
419
|
const payload = {
|
|
160
420
|
decision: { topic, title, date: stamp, status, summary, path: filePath, indexPath },
|
|
161
421
|
root: toRootOutput(root),
|
|
@@ -171,7 +431,7 @@ export async function decisionNewCommand(topic, options) {
|
|
|
171
431
|
}
|
|
172
432
|
catch (error) {
|
|
173
433
|
if (options.json) {
|
|
174
|
-
emitFailure(true,
|
|
434
|
+
emitFailure(true, failurePayload(), error, 'decision_error');
|
|
175
435
|
return;
|
|
176
436
|
}
|
|
177
437
|
throw error;
|
|
@@ -179,14 +439,36 @@ export async function decisionNewCommand(topic, options) {
|
|
|
179
439
|
}
|
|
180
440
|
export async function decisionListCommand(options) {
|
|
181
441
|
try {
|
|
442
|
+
// No implicit root, like `list` / `status --all` / `validate --all`: a batch
|
|
443
|
+
// query outside a project would answer "no decisions here" with exit 0, a
|
|
444
|
+
// clean report about somewhere never examined.
|
|
182
445
|
const root = await resolveRootForCommand(options, {
|
|
183
446
|
json: options.json,
|
|
184
|
-
|
|
447
|
+
allowImplicitRoot: false,
|
|
448
|
+
failurePayload: DECISION_LIST_FAILURE_PAYLOAD,
|
|
185
449
|
});
|
|
186
450
|
if (!root)
|
|
187
451
|
return;
|
|
188
452
|
const decisionsDir = path.join(root.path, DECISIONS_SUBDIR);
|
|
189
453
|
let entries = readDecisionEntries(decisionsDir);
|
|
454
|
+
// Taken before the filter, so the empty-state message can tell "nothing
|
|
455
|
+
// recorded" from "nothing matching".
|
|
456
|
+
const totalEntries = entries.length;
|
|
457
|
+
// The only write this command can perform, behind an explicit flag so a
|
|
458
|
+
// query never rewrites a tracked file unasked.
|
|
459
|
+
let reindexed = {
|
|
460
|
+
statusesRefreshed: 0,
|
|
461
|
+
rowsAdded: [],
|
|
462
|
+
danglingRows: [],
|
|
463
|
+
};
|
|
464
|
+
if (options.reindex) {
|
|
465
|
+
reindexed = reindexDecisionIndex(decisionsDir);
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
// Read-only: a query reports the rot it can see without rewriting a
|
|
469
|
+
// tracked file.
|
|
470
|
+
reindexed = { ...reindexed, danglingRows: findDanglingIndexRows(decisionsDir) };
|
|
471
|
+
}
|
|
190
472
|
if (options.status) {
|
|
191
473
|
entries = entries.filter((e) => e.status === options.status);
|
|
192
474
|
}
|
|
@@ -195,39 +477,85 @@ export async function decisionListCommand(options) {
|
|
|
195
477
|
entries.sort((a, b) => a.topic.localeCompare(b.topic));
|
|
196
478
|
}
|
|
197
479
|
else {
|
|
198
|
-
//
|
|
480
|
+
// Newest first; tie-break by topic for stability.
|
|
199
481
|
entries.sort((a, b) => b.date.localeCompare(a.date) || a.topic.localeCompare(b.topic));
|
|
200
482
|
}
|
|
201
483
|
if (options.json) {
|
|
202
|
-
emitSuccess({
|
|
484
|
+
emitSuccess({
|
|
485
|
+
decisions: entries,
|
|
486
|
+
...(options.reindex
|
|
487
|
+
? {
|
|
488
|
+
// `reindexedRows` keeps its original meaning (status cells
|
|
489
|
+
// rewritten); the other two drifts get their own fields.
|
|
490
|
+
reindexedRows: reindexed.statusesRefreshed,
|
|
491
|
+
addedRows: reindexed.rowsAdded,
|
|
492
|
+
danglingRows: reindexed.danglingRows,
|
|
493
|
+
}
|
|
494
|
+
: // Without --reindex the write counters would be a constant zero
|
|
495
|
+
// that reads as "checked, nothing to do"; only the drift this
|
|
496
|
+
// query actually looked for is reported, and only when there is
|
|
497
|
+
// some.
|
|
498
|
+
reindexed.danglingRows.length > 0
|
|
499
|
+
? { danglingRows: reindexed.danglingRows }
|
|
500
|
+
: {}),
|
|
501
|
+
}, toRootOutput(root));
|
|
203
502
|
return;
|
|
204
503
|
}
|
|
504
|
+
// Posix separators throughout: `DECISIONS_SUBDIR` is a `path.join`, so on
|
|
505
|
+
// Windows appending `/index.md` printed `tospec\decisions/index.md`.
|
|
506
|
+
const ledger = `${DECISIONS_SUBDIR.split(path.sep).join('/')}/index.md`;
|
|
507
|
+
const hasDangling = reindexed.danglingRows.length > 0;
|
|
508
|
+
if (options.reindex) {
|
|
509
|
+
const done = [];
|
|
510
|
+
if (reindexed.statusesRefreshed > 0) {
|
|
511
|
+
done.push(`refreshed ${reindexed.statusesRefreshed} status cell(s)`);
|
|
512
|
+
}
|
|
513
|
+
if (reindexed.rowsAdded.length > 0) {
|
|
514
|
+
done.push(`added ${reindexed.rowsAdded.length} missing row(s): ${reindexed.rowsAdded.join(', ')}`);
|
|
515
|
+
}
|
|
516
|
+
if (done.length > 0) {
|
|
517
|
+
console.log(`${ledger}: ${done.join('; ')}.`);
|
|
518
|
+
}
|
|
519
|
+
else if (!hasDangling) {
|
|
520
|
+
// The all-clear is withheld while a row points at a file that is gone:
|
|
521
|
+
// saying the ledger agrees with the decision files and then naming rows
|
|
522
|
+
// that do not is two answers to one question.
|
|
523
|
+
console.log(`${ledger} is already in sync with the decision files.`);
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
console.log(`${ledger}: no status cell or row needed rewriting, but it is not clean:`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (hasDangling) {
|
|
530
|
+
console.log(`Warning: ${reindexed.danglingRows.length} row(s) in ${ledger} link to a decision file that no longer exists: ${reindexed.danglingRows.join(', ')}.`);
|
|
531
|
+
console.log(options.reindex
|
|
532
|
+
? 'Their status reads "unknown". Restore the file or remove the row by hand.'
|
|
533
|
+
: 'Restore the file or remove the row by hand; `tospec decision list --reindex` reports them without deleting any.');
|
|
534
|
+
}
|
|
205
535
|
if (entries.length === 0) {
|
|
206
|
-
console.log(
|
|
536
|
+
console.log(options.status && totalEntries > 0
|
|
537
|
+
? `No decisions with status '${options.status}' (${totalEntries} recorded). Drop --status to see them all.`
|
|
538
|
+
: 'No decisions found. Create one with: tospec decision new <topic>');
|
|
207
539
|
return;
|
|
208
540
|
}
|
|
209
|
-
|
|
210
|
-
|
|
541
|
+
// Widths derived from the data: a hardcoded width never lined a
|
|
542
|
+
// `yyyyMMdd_HHmmss` stamp up with the rule beneath it.
|
|
543
|
+
const dateWidth = Math.max('Date'.length, ...entries.map((e) => e.date.length));
|
|
544
|
+
const statusWidth = Math.max('Status'.length, ...entries.map((e) => e.status.length));
|
|
545
|
+
console.log(`${'Date'.padEnd(dateWidth)} | ${'Status'.padEnd(statusWidth)} | Title`);
|
|
546
|
+
console.log(`${'-'.repeat(dateWidth)}-|-${'-'.repeat(statusWidth)}-|------`);
|
|
211
547
|
for (const e of entries) {
|
|
212
|
-
console.log(`${e.date} | ${e.status.padEnd(
|
|
548
|
+
console.log(`${e.date.padEnd(dateWidth)} | ${e.status.padEnd(statusWidth)} | ${e.title}`);
|
|
213
549
|
}
|
|
214
550
|
}
|
|
215
551
|
catch (error) {
|
|
216
552
|
if (options.json) {
|
|
217
|
-
emitFailure(true,
|
|
553
|
+
emitFailure(true, DECISION_LIST_FAILURE_PAYLOAD, error, 'decision_error');
|
|
218
554
|
return;
|
|
219
555
|
}
|
|
220
556
|
throw error;
|
|
221
557
|
}
|
|
222
558
|
}
|
|
223
|
-
// -----------------------------------------------------------------------------
|
|
224
|
-
// Registration
|
|
225
|
-
// -----------------------------------------------------------------------------
|
|
226
|
-
/**
|
|
227
|
-
* Register the `decision` command group and its subcommands.
|
|
228
|
-
*
|
|
229
|
-
* @param program - The Commander program instance
|
|
230
|
-
*/
|
|
231
559
|
export function registerDecisionCommand(program) {
|
|
232
560
|
const decisionCmd = program
|
|
233
561
|
.command('decision')
|
|
@@ -246,8 +574,10 @@ export function registerDecisionCommand(program) {
|
|
|
246
574
|
await decisionNewCommand(topic, options);
|
|
247
575
|
}
|
|
248
576
|
catch (error) {
|
|
249
|
-
|
|
250
|
-
|
|
577
|
+
// The command already emitted its own JSON envelope; this catch only
|
|
578
|
+
// reaches the human path, which uses the same `Error:` line as every
|
|
579
|
+
// other command.
|
|
580
|
+
emitFailure(false, {}, error, 'decision_error');
|
|
251
581
|
}
|
|
252
582
|
});
|
|
253
583
|
decisionCmd
|
|
@@ -255,14 +585,14 @@ export function registerDecisionCommand(program) {
|
|
|
255
585
|
.description('List decisions as an index table')
|
|
256
586
|
.addOption(new Option('--status <status>', 'Filter by status').choices(DECISION_STATUSES))
|
|
257
587
|
.addOption(new Option('--sort <mode>', 'Sort order').choices(['date', 'name']).default('date'))
|
|
588
|
+
.option('--reindex', "Rewrite decisions/index.md's status column from the decision files")
|
|
258
589
|
.option('--json', 'Output as JSON')
|
|
259
590
|
.action(async (options) => {
|
|
260
591
|
try {
|
|
261
592
|
await decisionListCommand(options);
|
|
262
593
|
}
|
|
263
594
|
catch (error) {
|
|
264
|
-
|
|
265
|
-
process.exitCode = 1;
|
|
595
|
+
emitFailure(false, {}, error, 'decision_error');
|
|
266
596
|
}
|
|
267
597
|
});
|
|
268
598
|
}
|