@seanmars/tospec 0.19.0-beta.7 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -231
- package/README.md +69 -82
- package/assets/dashboard/app.js +14 -3
- package/assets/dashboard/style.css +7 -0
- package/assets/rules/tospec/decision.md +3 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +227 -86
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/config.d.ts +9 -17
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +366 -107
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/dashboard.d.ts +57 -99
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dashboard.js +224 -282
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/decision.d.ts +41 -19
- package/dist/commands/decision.d.ts.map +1 -1
- package/dist/commands/decision.js +385 -79
- package/dist/commands/decision.js.map +1 -1
- package/dist/commands/metrics.d.ts +28 -51
- package/dist/commands/metrics.d.ts.map +1 -1
- package/dist/commands/metrics.js +62 -93
- package/dist/commands/metrics.js.map +1 -1
- package/dist/commands/shared-output.d.ts +18 -21
- package/dist/commands/shared-output.d.ts.map +1 -1
- package/dist/commands/shared-output.js +49 -23
- package/dist/commands/shared-output.js.map +1 -1
- package/dist/commands/show.d.ts +7 -0
- package/dist/commands/show.d.ts.map +1 -1
- package/dist/commands/show.js +29 -7
- package/dist/commands/show.js.map +1 -1
- package/dist/commands/validate.d.ts +43 -45
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +262 -129
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/workflow/index.d.ts +6 -10
- package/dist/commands/workflow/index.d.ts.map +1 -1
- package/dist/commands/workflow/index.js +6 -10
- package/dist/commands/workflow/index.js.map +1 -1
- package/dist/commands/workflow/instructions.d.ts +21 -8
- package/dist/commands/workflow/instructions.d.ts.map +1 -1
- package/dist/commands/workflow/instructions.js +254 -95
- package/dist/commands/workflow/instructions.js.map +1 -1
- package/dist/commands/workflow/new-change.d.ts +4 -5
- package/dist/commands/workflow/new-change.d.ts.map +1 -1
- package/dist/commands/workflow/new-change.js +90 -25
- package/dist/commands/workflow/new-change.js.map +1 -1
- package/dist/commands/workflow/schemas.d.ts +3 -5
- package/dist/commands/workflow/schemas.d.ts.map +1 -1
- package/dist/commands/workflow/schemas.js +37 -11
- package/dist/commands/workflow/schemas.js.map +1 -1
- package/dist/commands/workflow/shared.d.ts +48 -21
- package/dist/commands/workflow/shared.d.ts.map +1 -1
- package/dist/commands/workflow/shared.js +36 -33
- package/dist/commands/workflow/shared.js.map +1 -1
- package/dist/commands/workflow/status.d.ts +10 -6
- package/dist/commands/workflow/status.d.ts.map +1 -1
- package/dist/commands/workflow/status.js +81 -42
- package/dist/commands/workflow/status.js.map +1 -1
- package/dist/commands/workflow/templates.d.ts +10 -3
- package/dist/commands/workflow/templates.d.ts.map +1 -1
- package/dist/commands/workflow/templates.js +39 -40
- package/dist/commands/workflow/templates.js.map +1 -1
- package/dist/core/archive.d.ts +26 -21
- package/dist/core/archive.d.ts.map +1 -1
- package/dist/core/archive.js +378 -235
- package/dist/core/archive.js.map +1 -1
- package/dist/core/artifact-graph/graph.d.ts +25 -42
- package/dist/core/artifact-graph/graph.d.ts.map +1 -1
- package/dist/core/artifact-graph/graph.js +45 -63
- package/dist/core/artifact-graph/graph.js.map +1 -1
- package/dist/core/artifact-graph/index.d.ts +1 -1
- package/dist/core/artifact-graph/index.d.ts.map +1 -1
- package/dist/core/artifact-graph/index.js +1 -1
- package/dist/core/artifact-graph/index.js.map +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +97 -103
- package/dist/core/artifact-graph/instruction-loader.d.ts.map +1 -1
- package/dist/core/artifact-graph/instruction-loader.js +146 -119
- package/dist/core/artifact-graph/instruction-loader.js.map +1 -1
- package/dist/core/artifact-graph/outputs.d.ts +9 -23
- package/dist/core/artifact-graph/outputs.d.ts.map +1 -1
- package/dist/core/artifact-graph/outputs.js +45 -38
- package/dist/core/artifact-graph/outputs.js.map +1 -1
- package/dist/core/artifact-graph/resolver.d.ts +44 -63
- package/dist/core/artifact-graph/resolver.d.ts.map +1 -1
- package/dist/core/artifact-graph/resolver.js +85 -86
- package/dist/core/artifact-graph/resolver.js.map +1 -1
- package/dist/core/artifact-graph/schema.d.ts +0 -6
- package/dist/core/artifact-graph/schema.d.ts.map +1 -1
- package/dist/core/artifact-graph/schema.js +7 -32
- package/dist/core/artifact-graph/schema.js.map +1 -1
- package/dist/core/artifact-graph/state.d.ts +1 -8
- package/dist/core/artifact-graph/state.d.ts.map +1 -1
- package/dist/core/artifact-graph/state.js +2 -17
- package/dist/core/artifact-graph/state.js.map +1 -1
- package/dist/core/artifact-graph/stub-detection.d.ts +12 -0
- package/dist/core/artifact-graph/stub-detection.d.ts.map +1 -0
- package/dist/core/artifact-graph/stub-detection.js +39 -0
- package/dist/core/artifact-graph/stub-detection.js.map +1 -0
- package/dist/core/artifact-graph/types.d.ts +4 -0
- package/dist/core/artifact-graph/types.d.ts.map +1 -1
- package/dist/core/artifact-graph/types.js +30 -10
- package/dist/core/artifact-graph/types.js.map +1 -1
- package/dist/core/available-tools.d.ts +3 -12
- package/dist/core/available-tools.d.ts.map +1 -1
- package/dist/core/available-tools.js +4 -13
- package/dist/core/available-tools.js.map +1 -1
- package/dist/core/change-metadata/schema.d.ts +1 -1
- package/dist/core/change-metadata/schema.d.ts.map +1 -1
- package/dist/core/change-metadata/schema.js +10 -7
- package/dist/core/change-metadata/schema.js.map +1 -1
- package/dist/core/change-presenter.d.ts +23 -18
- package/dist/core/change-presenter.d.ts.map +1 -1
- package/dist/core/change-presenter.js +102 -43
- package/dist/core/change-presenter.js.map +1 -1
- package/dist/core/change-status-policy.d.ts +8 -1
- package/dist/core/change-status-policy.d.ts.map +1 -1
- package/dist/core/change-status-policy.js +25 -1
- package/dist/core/change-status-policy.js.map +1 -1
- package/dist/core/codex-metrics.d.ts +25 -45
- package/dist/core/codex-metrics.d.ts.map +1 -1
- package/dist/core/codex-metrics.js +44 -88
- package/dist/core/codex-metrics.js.map +1 -1
- package/dist/core/codex-residue.d.ts +14 -15
- package/dist/core/codex-residue.d.ts.map +1 -1
- package/dist/core/codex-residue.js +18 -22
- package/dist/core/codex-residue.js.map +1 -1
- package/dist/core/command-generation/adapters/claude.d.ts +2 -9
- package/dist/core/command-generation/adapters/claude.d.ts.map +1 -1
- package/dist/core/command-generation/adapters/claude.js +2 -12
- package/dist/core/command-generation/adapters/claude.js.map +1 -1
- package/dist/core/command-generation/adapters/index.d.ts +1 -9
- package/dist/core/command-generation/adapters/index.d.ts.map +1 -1
- package/dist/core/command-generation/adapters/index.js +1 -9
- package/dist/core/command-generation/adapters/index.js.map +1 -1
- package/dist/core/command-generation/generator.d.ts +0 -17
- package/dist/core/command-generation/generator.d.ts.map +1 -1
- package/dist/core/command-generation/generator.js +0 -17
- package/dist/core/command-generation/generator.js.map +1 -1
- package/dist/core/command-generation/index.d.ts +2 -5
- package/dist/core/command-generation/index.d.ts.map +1 -1
- package/dist/core/command-generation/index.js +0 -9
- package/dist/core/command-generation/index.js.map +1 -1
- package/dist/core/command-generation/types.d.ts +10 -36
- package/dist/core/command-generation/types.d.ts.map +1 -1
- package/dist/core/command-generation/types.js +0 -6
- package/dist/core/command-generation/types.js.map +1 -1
- package/dist/core/command-generation/yaml.d.ts +3 -18
- package/dist/core/command-generation/yaml.d.ts.map +1 -1
- package/dist/core/command-generation/yaml.js +5 -23
- package/dist/core/command-generation/yaml.js.map +1 -1
- package/dist/core/config-prompts.d.ts +2 -4
- package/dist/core/config-prompts.d.ts.map +1 -1
- package/dist/core/config-prompts.js +2 -7
- package/dist/core/config-prompts.js.map +1 -1
- package/dist/core/config-schema.d.ts +8 -53
- package/dist/core/config-schema.d.ts.map +1 -1
- package/dist/core/config-schema.js +49 -62
- package/dist/core/config-schema.js.map +1 -1
- package/dist/core/config.d.ts +39 -26
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +36 -22
- package/dist/core/config.js.map +1 -1
- package/dist/core/dashboard-activity.d.ts +7 -9
- package/dist/core/dashboard-activity.d.ts.map +1 -1
- package/dist/core/dashboard-activity.js +26 -24
- package/dist/core/dashboard-activity.js.map +1 -1
- package/dist/core/dashboard-data.d.ts +40 -22
- package/dist/core/dashboard-data.d.ts.map +1 -1
- package/dist/core/dashboard-data.js +62 -70
- package/dist/core/dashboard-data.js.map +1 -1
- package/dist/core/global-config.d.ts +24 -53
- package/dist/core/global-config.d.ts.map +1 -1
- package/dist/core/global-config.js +38 -67
- package/dist/core/global-config.js.map +1 -1
- package/dist/core/init.d.ts +25 -19
- package/dist/core/init.d.ts.map +1 -1
- package/dist/core/init.js +168 -181
- package/dist/core/init.js.map +1 -1
- package/dist/core/list.d.ts +1 -1
- package/dist/core/list.d.ts.map +1 -1
- package/dist/core/list.js +121 -28
- package/dist/core/list.js.map +1 -1
- package/dist/core/local-server.d.ts +41 -83
- package/dist/core/local-server.d.ts.map +1 -1
- package/dist/core/local-server.js +53 -98
- package/dist/core/local-server.js.map +1 -1
- package/dist/core/markdown-render.d.ts +25 -0
- package/dist/core/markdown-render.d.ts.map +1 -0
- package/dist/core/markdown-render.js +94 -0
- package/dist/core/markdown-render.js.map +1 -0
- package/dist/core/migrate.d.ts +29 -14
- package/dist/core/migrate.d.ts.map +1 -1
- package/dist/core/migrate.js +184 -122
- package/dist/core/migrate.js.map +1 -1
- package/dist/core/parsers/change-parser.d.ts +7 -10
- package/dist/core/parsers/change-parser.d.ts.map +1 -1
- package/dist/core/parsers/change-parser.js +48 -56
- package/dist/core/parsers/change-parser.js.map +1 -1
- package/dist/core/parsers/markdown-parser.d.ts +8 -9
- package/dist/core/parsers/markdown-parser.d.ts.map +1 -1
- package/dist/core/parsers/markdown-parser.js +31 -22
- package/dist/core/parsers/markdown-parser.js.map +1 -1
- package/dist/core/parsers/requirement-blocks.d.ts +43 -15
- package/dist/core/parsers/requirement-blocks.d.ts.map +1 -1
- package/dist/core/parsers/requirement-blocks.js +142 -70
- package/dist/core/parsers/requirement-blocks.js.map +1 -1
- package/dist/core/parsers/requirement-text.d.ts +73 -79
- package/dist/core/parsers/requirement-text.d.ts.map +1 -1
- package/dist/core/parsers/requirement-text.js +137 -79
- package/dist/core/parsers/requirement-text.js.map +1 -1
- package/dist/core/parsers/spec-structure.d.ts.map +1 -1
- package/dist/core/parsers/spec-structure.js +10 -6
- package/dist/core/parsers/spec-structure.js.map +1 -1
- package/dist/core/profiles.d.ts +3 -10
- package/dist/core/profiles.d.ts.map +1 -1
- package/dist/core/profiles.js +5 -12
- package/dist/core/profiles.js.map +1 -1
- package/dist/core/project-config.d.ts +43 -44
- package/dist/core/project-config.d.ts.map +1 -1
- package/dist/core/project-config.js +107 -82
- package/dist/core/project-config.js.map +1 -1
- package/dist/core/project-layout.d.ts +9 -17
- package/dist/core/project-layout.d.ts.map +1 -1
- package/dist/core/project-layout.js +16 -26
- package/dist/core/project-layout.js.map +1 -1
- package/dist/core/root-selection.d.ts +11 -7
- package/dist/core/root-selection.d.ts.map +1 -1
- package/dist/core/root-selection.js +7 -8
- package/dist/core/root-selection.js.map +1 -1
- package/dist/core/rules.d.ts +6 -1
- package/dist/core/rules.d.ts.map +1 -1
- package/dist/core/rules.js +23 -2
- package/dist/core/rules.js.map +1 -1
- package/dist/core/schema-names.d.ts +16 -0
- package/dist/core/schema-names.d.ts.map +1 -0
- package/dist/core/schema-names.js +16 -0
- package/dist/core/schema-names.js.map +1 -0
- package/dist/core/schemas/base.schema.d.ts +3 -0
- package/dist/core/schemas/base.schema.d.ts.map +1 -1
- package/dist/core/schemas/base.schema.js +22 -6
- package/dist/core/schemas/base.schema.js.map +1 -1
- package/dist/core/schemas/change.schema.d.ts +16 -0
- package/dist/core/schemas/change.schema.d.ts.map +1 -1
- package/dist/core/schemas/change.schema.js +41 -10
- package/dist/core/schemas/change.schema.js.map +1 -1
- package/dist/core/schemas/spec.schema.d.ts +2 -0
- package/dist/core/schemas/spec.schema.d.ts.map +1 -1
- package/dist/core/shared/index.d.ts +3 -8
- package/dist/core/shared/index.d.ts.map +1 -1
- package/dist/core/shared/index.js +3 -8
- package/dist/core/shared/index.js.map +1 -1
- package/dist/core/shared/rules-generation.d.ts +19 -8
- package/dist/core/shared/rules-generation.d.ts.map +1 -1
- package/dist/core/shared/rules-generation.js +117 -50
- package/dist/core/shared/rules-generation.js.map +1 -1
- package/dist/core/shared/skill-generation.d.ts +28 -43
- package/dist/core/shared/skill-generation.d.ts.map +1 -1
- package/dist/core/shared/skill-generation.js +82 -51
- package/dist/core/shared/skill-generation.js.map +1 -1
- package/dist/core/shared/tool-detection.d.ts +39 -61
- package/dist/core/shared/tool-detection.d.ts.map +1 -1
- package/dist/core/shared/tool-detection.js +88 -80
- package/dist/core/shared/tool-detection.js.map +1 -1
- package/dist/core/skill-metrics.d.ts +36 -63
- package/dist/core/skill-metrics.d.ts.map +1 -1
- package/dist/core/skill-metrics.js +34 -73
- package/dist/core/skill-metrics.js.map +1 -1
- package/dist/core/spec-presenter.js +6 -6
- package/dist/core/spec-presenter.js.map +1 -1
- package/dist/core/specs-apply.d.ts +22 -23
- package/dist/core/specs-apply.d.ts.map +1 -1
- package/dist/core/specs-apply.js +166 -193
- package/dist/core/specs-apply.js.map +1 -1
- package/dist/core/templates/fragments/interview.d.ts +2 -6
- package/dist/core/templates/fragments/interview.d.ts.map +1 -1
- package/dist/core/templates/fragments/interview.js +2 -6
- package/dist/core/templates/fragments/interview.js.map +1 -1
- package/dist/core/templates/fragments/next-step.d.ts +4 -8
- package/dist/core/templates/fragments/next-step.d.ts.map +1 -1
- package/dist/core/templates/fragments/next-step.js +4 -8
- package/dist/core/templates/fragments/next-step.js.map +1 -1
- package/dist/core/templates/fragments/validate.d.ts +13 -0
- package/dist/core/templates/fragments/validate.d.ts.map +1 -0
- package/dist/core/templates/fragments/validate.js +13 -0
- package/dist/core/templates/fragments/validate.js.map +1 -0
- package/dist/core/templates/fragments/verify.d.ts +9 -12
- package/dist/core/templates/fragments/verify.d.ts.map +1 -1
- package/dist/core/templates/fragments/verify.js +9 -12
- package/dist/core/templates/fragments/verify.js.map +1 -1
- package/dist/core/templates/index.d.ts +0 -6
- package/dist/core/templates/index.d.ts.map +1 -1
- package/dist/core/templates/index.js +0 -7
- package/dist/core/templates/index.js.map +1 -1
- package/dist/core/templates/skill-templates.d.ts +1 -5
- package/dist/core/templates/skill-templates.d.ts.map +1 -1
- package/dist/core/templates/skill-templates.js +0 -5
- package/dist/core/templates/skill-templates.js.map +1 -1
- package/dist/core/templates/types.d.ts +3 -7
- package/dist/core/templates/types.d.ts.map +1 -1
- package/dist/core/templates/types.js +0 -3
- package/dist/core/templates/types.js.map +1 -1
- package/dist/core/templates/workflows/apply.d.ts +3 -9
- package/dist/core/templates/workflows/apply.d.ts.map +1 -1
- package/dist/core/templates/workflows/apply.js +11 -13
- package/dist/core/templates/workflows/apply.js.map +1 -1
- package/dist/core/templates/workflows/archive.d.ts +0 -6
- package/dist/core/templates/workflows/archive.d.ts.map +1 -1
- package/dist/core/templates/workflows/archive.js +19 -6
- package/dist/core/templates/workflows/archive.js.map +1 -1
- package/dist/core/templates/workflows/decision.js +4 -4
- package/dist/core/templates/workflows/decision.js.map +1 -1
- package/dist/core/templates/workflows/explore.js +1 -1
- package/dist/core/templates/workflows/grill.d.ts.map +1 -1
- package/dist/core/templates/workflows/grill.js +0 -2
- package/dist/core/templates/workflows/grill.js.map +1 -1
- package/dist/core/templates/workflows/issue.d.ts +0 -6
- package/dist/core/templates/workflows/issue.d.ts.map +1 -1
- package/dist/core/templates/workflows/issue.js +3 -2
- package/dist/core/templates/workflows/issue.js.map +1 -1
- package/dist/core/templates/workflows/propose.d.ts +0 -6
- package/dist/core/templates/workflows/propose.d.ts.map +1 -1
- package/dist/core/templates/workflows/propose.js +2 -2
- package/dist/core/templates/workflows/propose.js.map +1 -1
- package/dist/core/templates/workflows/sync.d.ts +2 -8
- package/dist/core/templates/workflows/sync.d.ts.map +1 -1
- package/dist/core/templates/workflows/sync.js +4 -3
- package/dist/core/templates/workflows/sync.js.map +1 -1
- package/dist/core/templates/workflows/update.d.ts +0 -6
- package/dist/core/templates/workflows/update.d.ts.map +1 -1
- package/dist/core/templates/workflows/update.js +9 -2
- package/dist/core/templates/workflows/update.js.map +1 -1
- package/dist/core/update.d.ts +23 -23
- package/dist/core/update.d.ts.map +1 -1
- package/dist/core/update.js +139 -118
- package/dist/core/update.js.map +1 -1
- package/dist/core/user-state-migration.d.ts +13 -15
- package/dist/core/user-state-migration.d.ts.map +1 -1
- package/dist/core/user-state-migration.js +16 -20
- package/dist/core/user-state-migration.js.map +1 -1
- package/dist/core/validation/constants.d.ts +4 -10
- package/dist/core/validation/constants.d.ts.map +1 -1
- package/dist/core/validation/constants.js +25 -20
- package/dist/core/validation/constants.js.map +1 -1
- package/dist/core/validation/prose-length.d.ts +15 -0
- package/dist/core/validation/prose-length.d.ts.map +1 -0
- package/dist/core/validation/prose-length.js +29 -0
- package/dist/core/validation/prose-length.js.map +1 -0
- package/dist/core/validation/purpose-placeholder.d.ts +9 -16
- package/dist/core/validation/purpose-placeholder.d.ts.map +1 -1
- package/dist/core/validation/purpose-placeholder.js +30 -44
- package/dist/core/validation/purpose-placeholder.js.map +1 -1
- package/dist/core/validation/section-validator.d.ts +4 -4
- package/dist/core/validation/section-validator.d.ts.map +1 -1
- package/dist/core/validation/section-validator.js +43 -7
- package/dist/core/validation/section-validator.js.map +1 -1
- package/dist/core/validation/task-numbering.d.ts +6 -3
- package/dist/core/validation/task-numbering.d.ts.map +1 -1
- package/dist/core/validation/task-numbering.js +23 -11
- package/dist/core/validation/task-numbering.js.map +1 -1
- package/dist/core/validation/types.d.ts +18 -0
- package/dist/core/validation/types.d.ts.map +1 -1
- package/dist/core/validation/types.js +12 -1
- package/dist/core/validation/types.js.map +1 -1
- package/dist/core/validation/validator.d.ts +43 -63
- package/dist/core/validation/validator.d.ts.map +1 -1
- package/dist/core/validation/validator.js +500 -267
- package/dist/core/validation/validator.js.map +1 -1
- package/dist/prompts/searchable-multi-select.d.ts +3 -8
- package/dist/prompts/searchable-multi-select.d.ts.map +1 -1
- package/dist/prompts/searchable-multi-select.js +16 -39
- package/dist/prompts/searchable-multi-select.js.map +1 -1
- package/dist/utils/change-metadata.d.ts +11 -50
- package/dist/utils/change-metadata.d.ts.map +1 -1
- package/dist/utils/change-metadata.js +48 -67
- package/dist/utils/change-metadata.js.map +1 -1
- package/dist/utils/change-utils.d.ts +30 -54
- package/dist/utils/change-utils.d.ts.map +1 -1
- package/dist/utils/change-utils.js +133 -91
- package/dist/utils/change-utils.js.map +1 -1
- package/dist/utils/file-lock.d.ts +39 -0
- package/dist/utils/file-lock.d.ts.map +1 -0
- package/dist/utils/file-lock.js +149 -0
- package/dist/utils/file-lock.js.map +1 -0
- package/dist/utils/file-system.d.ts +12 -32
- package/dist/utils/file-system.d.ts.map +1 -1
- package/dist/utils/file-system.js +16 -40
- package/dist/utils/file-system.js.map +1 -1
- package/dist/utils/frontmatter.d.ts +7 -11
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +11 -11
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/interactive.d.ts +4 -9
- package/dist/utils/interactive.d.ts.map +1 -1
- package/dist/utils/interactive.js +2 -4
- package/dist/utils/interactive.js.map +1 -1
- package/dist/utils/item-discovery.d.ts +15 -10
- package/dist/utils/item-discovery.d.ts.map +1 -1
- package/dist/utils/item-discovery.js +42 -47
- package/dist/utils/item-discovery.js.map +1 -1
- package/dist/utils/link.d.ts +9 -18
- package/dist/utils/link.d.ts.map +1 -1
- package/dist/utils/link.js +9 -18
- package/dist/utils/link.js.map +1 -1
- package/dist/utils/requirement-diff.d.ts +13 -23
- package/dist/utils/requirement-diff.d.ts.map +1 -1
- package/dist/utils/requirement-diff.js +13 -23
- package/dist/utils/requirement-diff.js.map +1 -1
- package/dist/utils/spec-files.d.ts +7 -20
- package/dist/utils/spec-files.d.ts.map +1 -1
- package/dist/utils/spec-files.js +26 -51
- package/dist/utils/spec-files.js.map +1 -1
- package/dist/utils/task-progress.d.ts +11 -9
- package/dist/utils/task-progress.d.ts.map +1 -1
- package/dist/utils/task-progress.js +53 -32
- package/dist/utils/task-progress.js.map +1 -1
- package/dist/utils/timestamp.d.ts +5 -8
- package/dist/utils/timestamp.d.ts.map +1 -1
- package/dist/utils/timestamp.js +5 -8
- package/dist/utils/timestamp.js.map +1 -1
- package/package.json +2 -3
- package/schemas/decision/templates/decision.md +3 -1
- package/schemas/decision/templates/index.md +2 -2
- package/schemas/issue/schema.yaml +8 -1
- package/schemas/issue/templates/spec.md +27 -2
- package/schemas/sdd/schema.yaml +20 -1
- package/schemas/sdd/templates/spec.md +27 -2
- /package/assets/rules/tospec/{single-sourc-of-truth.md → single-source-of-truth.md} +0 -0
|
@@ -1,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,243 @@ function ensureIndex(decisionsDir, projectRoot) {
|
|
|
107
128
|
}
|
|
108
129
|
return indexPath;
|
|
109
130
|
}
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
// the ledger ever grows noisy enough to matter.
|
|
115
|
-
// `file` is the sibling filename (index.md lives in the same decisions dir), so
|
|
116
|
-
// a relative markdown link resolves straight to the decision doc.
|
|
117
|
-
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) {
|
|
118
135
|
const fileLink = `[${indexCell(file)}](${file})`;
|
|
119
|
-
|
|
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 };
|
|
120
348
|
}
|
|
121
|
-
// -----------------------------------------------------------------------------
|
|
122
|
-
// Command implementations (exported for tests)
|
|
123
|
-
// -----------------------------------------------------------------------------
|
|
124
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
|
+
});
|
|
125
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;
|
|
126
368
|
if (!topic)
|
|
127
369
|
throw new Error('Missing required argument <topic>');
|
|
128
370
|
const nameValidation = validateChangeName(topic, 'Topic');
|
|
@@ -137,33 +379,29 @@ export async function decisionNewCommand(topic, options) {
|
|
|
137
379
|
if (!TIMESTAMP_RE.test(stamp)) {
|
|
138
380
|
throw new Error(`Invalid --date '${stamp}'. Expected project timestamp format: yyyyMMdd_HHmmss`);
|
|
139
381
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
});
|
|
144
|
-
if (!root)
|
|
145
|
-
return;
|
|
382
|
+
if (!isRealTimestamp(stamp)) {
|
|
383
|
+
throw new Error(`Invalid --date '${stamp}'. That is not a real date and time.`);
|
|
384
|
+
}
|
|
146
385
|
const decisionsDir = path.join(root.path, DECISIONS_SUBDIR);
|
|
147
386
|
const fileName = `${stamp}-${topic}.md`;
|
|
148
387
|
const filePath = path.join(decisionsDir, fileName);
|
|
149
|
-
// Only --force needs this read: its write path (below) is the plain 'w'
|
|
150
|
-
// flag, so it is the one case where "did the file already exist" cannot be
|
|
151
|
-
// recovered from the write's own outcome.
|
|
152
|
-
const existedBeforeWrite = options.force ? fs.existsSync(filePath) : false;
|
|
153
388
|
const title = options.title ?? topic;
|
|
154
389
|
const summary = options.summary ?? title;
|
|
155
390
|
const template = loadTemplate(SCHEMA_NAME, 'decision.md', root.path);
|
|
156
391
|
const content = renderDecision(template, title, stamp, status);
|
|
157
|
-
//
|
|
158
|
-
//
|
|
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);
|
|
159
398
|
fs.mkdirSync(decisionsDir, { recursive: true });
|
|
160
399
|
if (options.force) {
|
|
161
400
|
fs.writeFileSync(filePath, content, 'utf-8');
|
|
162
401
|
}
|
|
163
402
|
else {
|
|
164
|
-
// 'wx' makes the create atomic
|
|
165
|
-
//
|
|
166
|
-
// check would leave open between two concurrent invocations.
|
|
403
|
+
// 'wx' makes the create atomic, closing the check-then-write window a
|
|
404
|
+
// separate existsSync would leave open.
|
|
167
405
|
try {
|
|
168
406
|
fs.writeFileSync(filePath, content, { encoding: 'utf-8', flag: 'wx' });
|
|
169
407
|
}
|
|
@@ -175,11 +413,9 @@ export async function decisionNewCommand(topic, options) {
|
|
|
175
413
|
}
|
|
176
414
|
}
|
|
177
415
|
const indexPath = ensureIndex(decisionsDir, root.path);
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
appendIndexRow(indexPath, stampToHuman(stamp), title, summary, fileName);
|
|
182
|
-
}
|
|
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);
|
|
183
419
|
const payload = {
|
|
184
420
|
decision: { topic, title, date: stamp, status, summary, path: filePath, indexPath },
|
|
185
421
|
root: toRootOutput(root),
|
|
@@ -195,7 +431,7 @@ export async function decisionNewCommand(topic, options) {
|
|
|
195
431
|
}
|
|
196
432
|
catch (error) {
|
|
197
433
|
if (options.json) {
|
|
198
|
-
emitFailure(true,
|
|
434
|
+
emitFailure(true, failurePayload(), error, 'decision_error');
|
|
199
435
|
return;
|
|
200
436
|
}
|
|
201
437
|
throw error;
|
|
@@ -203,14 +439,36 @@ export async function decisionNewCommand(topic, options) {
|
|
|
203
439
|
}
|
|
204
440
|
export async function decisionListCommand(options) {
|
|
205
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.
|
|
206
445
|
const root = await resolveRootForCommand(options, {
|
|
207
446
|
json: options.json,
|
|
208
|
-
|
|
447
|
+
allowImplicitRoot: false,
|
|
448
|
+
failurePayload: DECISION_LIST_FAILURE_PAYLOAD,
|
|
209
449
|
});
|
|
210
450
|
if (!root)
|
|
211
451
|
return;
|
|
212
452
|
const decisionsDir = path.join(root.path, DECISIONS_SUBDIR);
|
|
213
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
|
+
}
|
|
214
472
|
if (options.status) {
|
|
215
473
|
entries = entries.filter((e) => e.status === options.status);
|
|
216
474
|
}
|
|
@@ -219,39 +477,85 @@ export async function decisionListCommand(options) {
|
|
|
219
477
|
entries.sort((a, b) => a.topic.localeCompare(b.topic));
|
|
220
478
|
}
|
|
221
479
|
else {
|
|
222
|
-
//
|
|
480
|
+
// Newest first; tie-break by topic for stability.
|
|
223
481
|
entries.sort((a, b) => b.date.localeCompare(a.date) || a.topic.localeCompare(b.topic));
|
|
224
482
|
}
|
|
225
483
|
if (options.json) {
|
|
226
|
-
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));
|
|
227
502
|
return;
|
|
228
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
|
+
}
|
|
229
535
|
if (entries.length === 0) {
|
|
230
|
-
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>');
|
|
231
539
|
return;
|
|
232
540
|
}
|
|
233
|
-
|
|
234
|
-
|
|
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)}-|------`);
|
|
235
547
|
for (const e of entries) {
|
|
236
|
-
console.log(`${e.date} | ${e.status.padEnd(
|
|
548
|
+
console.log(`${e.date.padEnd(dateWidth)} | ${e.status.padEnd(statusWidth)} | ${e.title}`);
|
|
237
549
|
}
|
|
238
550
|
}
|
|
239
551
|
catch (error) {
|
|
240
552
|
if (options.json) {
|
|
241
|
-
emitFailure(true,
|
|
553
|
+
emitFailure(true, DECISION_LIST_FAILURE_PAYLOAD, error, 'decision_error');
|
|
242
554
|
return;
|
|
243
555
|
}
|
|
244
556
|
throw error;
|
|
245
557
|
}
|
|
246
558
|
}
|
|
247
|
-
// -----------------------------------------------------------------------------
|
|
248
|
-
// Registration
|
|
249
|
-
// -----------------------------------------------------------------------------
|
|
250
|
-
/**
|
|
251
|
-
* Register the `decision` command group and its subcommands.
|
|
252
|
-
*
|
|
253
|
-
* @param program - The Commander program instance
|
|
254
|
-
*/
|
|
255
559
|
export function registerDecisionCommand(program) {
|
|
256
560
|
const decisionCmd = program
|
|
257
561
|
.command('decision')
|
|
@@ -270,8 +574,10 @@ export function registerDecisionCommand(program) {
|
|
|
270
574
|
await decisionNewCommand(topic, options);
|
|
271
575
|
}
|
|
272
576
|
catch (error) {
|
|
273
|
-
|
|
274
|
-
|
|
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');
|
|
275
581
|
}
|
|
276
582
|
});
|
|
277
583
|
decisionCmd
|
|
@@ -279,14 +585,14 @@ export function registerDecisionCommand(program) {
|
|
|
279
585
|
.description('List decisions as an index table')
|
|
280
586
|
.addOption(new Option('--status <status>', 'Filter by status').choices(DECISION_STATUSES))
|
|
281
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")
|
|
282
589
|
.option('--json', 'Output as JSON')
|
|
283
590
|
.action(async (options) => {
|
|
284
591
|
try {
|
|
285
592
|
await decisionListCommand(options);
|
|
286
593
|
}
|
|
287
594
|
catch (error) {
|
|
288
|
-
|
|
289
|
-
process.exitCode = 1;
|
|
595
|
+
emitFailure(false, {}, error, 'decision_error');
|
|
290
596
|
}
|
|
291
597
|
});
|
|
292
598
|
}
|