@seanmars/tospec 0.19.0-beta.8 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -310
- package/README.md +69 -82
- package/assets/dashboard/app.js +11 -0
- package/assets/dashboard/style.css +7 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +89 -106
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/config.d.ts +9 -17
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +293 -145
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/dashboard.d.ts +57 -99
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dashboard.js +190 -313
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/decision.d.ts +38 -27
- package/dist/commands/decision.d.ts.map +1 -1
- package/dist/commands/decision.js +296 -128
- package/dist/commands/decision.js.map +1 -1
- package/dist/commands/metrics.d.ts +28 -51
- package/dist/commands/metrics.d.ts.map +1 -1
- package/dist/commands/metrics.js +62 -93
- package/dist/commands/metrics.js.map +1 -1
- package/dist/commands/shared-output.d.ts +12 -27
- package/dist/commands/shared-output.d.ts.map +1 -1
- package/dist/commands/shared-output.js +22 -45
- package/dist/commands/shared-output.js.map +1 -1
- package/dist/commands/show.d.ts +4 -7
- package/dist/commands/show.d.ts.map +1 -1
- package/dist/commands/show.js +23 -11
- package/dist/commands/show.js.map +1 -1
- package/dist/commands/validate.d.ts +34 -58
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +225 -141
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/workflow/index.d.ts +1 -5
- package/dist/commands/workflow/index.d.ts.map +1 -1
- package/dist/commands/workflow/index.js +1 -5
- package/dist/commands/workflow/index.js.map +1 -1
- package/dist/commands/workflow/instructions.d.ts +14 -24
- package/dist/commands/workflow/instructions.d.ts.map +1 -1
- package/dist/commands/workflow/instructions.js +219 -128
- package/dist/commands/workflow/instructions.js.map +1 -1
- package/dist/commands/workflow/new-change.d.ts +2 -5
- package/dist/commands/workflow/new-change.d.ts.map +1 -1
- package/dist/commands/workflow/new-change.js +69 -32
- package/dist/commands/workflow/new-change.js.map +1 -1
- package/dist/commands/workflow/schemas.d.ts +1 -5
- package/dist/commands/workflow/schemas.d.ts.map +1 -1
- package/dist/commands/workflow/schemas.js +6 -17
- package/dist/commands/workflow/schemas.js.map +1 -1
- package/dist/commands/workflow/shared.d.ts +37 -42
- package/dist/commands/workflow/shared.d.ts.map +1 -1
- package/dist/commands/workflow/shared.js +23 -54
- package/dist/commands/workflow/shared.js.map +1 -1
- package/dist/commands/workflow/status.d.ts +7 -17
- package/dist/commands/workflow/status.d.ts.map +1 -1
- package/dist/commands/workflow/status.js +57 -72
- package/dist/commands/workflow/status.js.map +1 -1
- package/dist/commands/workflow/templates.d.ts +8 -8
- package/dist/commands/workflow/templates.d.ts.map +1 -1
- package/dist/commands/workflow/templates.js +32 -46
- package/dist/commands/workflow/templates.js.map +1 -1
- package/dist/core/archive.d.ts +22 -31
- package/dist/core/archive.d.ts.map +1 -1
- package/dist/core/archive.js +296 -283
- package/dist/core/archive.js.map +1 -1
- package/dist/core/artifact-graph/graph.d.ts +25 -42
- package/dist/core/artifact-graph/graph.d.ts.map +1 -1
- package/dist/core/artifact-graph/graph.js +45 -63
- package/dist/core/artifact-graph/graph.js.map +1 -1
- package/dist/core/artifact-graph/index.d.ts +1 -1
- package/dist/core/artifact-graph/index.d.ts.map +1 -1
- package/dist/core/artifact-graph/index.js +1 -1
- package/dist/core/artifact-graph/index.js.map +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +54 -120
- package/dist/core/artifact-graph/instruction-loader.d.ts.map +1 -1
- package/dist/core/artifact-graph/instruction-loader.js +129 -111
- package/dist/core/artifact-graph/instruction-loader.js.map +1 -1
- package/dist/core/artifact-graph/outputs.d.ts +9 -23
- package/dist/core/artifact-graph/outputs.d.ts.map +1 -1
- package/dist/core/artifact-graph/outputs.js +45 -38
- package/dist/core/artifact-graph/outputs.js.map +1 -1
- package/dist/core/artifact-graph/resolver.d.ts +36 -81
- package/dist/core/artifact-graph/resolver.d.ts.map +1 -1
- package/dist/core/artifact-graph/resolver.js +60 -101
- package/dist/core/artifact-graph/resolver.js.map +1 -1
- package/dist/core/artifact-graph/schema.d.ts +0 -6
- package/dist/core/artifact-graph/schema.d.ts.map +1 -1
- package/dist/core/artifact-graph/schema.js +7 -32
- package/dist/core/artifact-graph/schema.js.map +1 -1
- package/dist/core/artifact-graph/state.d.ts +1 -8
- package/dist/core/artifact-graph/state.d.ts.map +1 -1
- package/dist/core/artifact-graph/state.js +2 -17
- package/dist/core/artifact-graph/state.js.map +1 -1
- package/dist/core/artifact-graph/stub-detection.d.ts +6 -14
- package/dist/core/artifact-graph/stub-detection.d.ts.map +1 -1
- package/dist/core/artifact-graph/stub-detection.js +13 -16
- package/dist/core/artifact-graph/stub-detection.js.map +1 -1
- package/dist/core/artifact-graph/types.d.ts +4 -0
- package/dist/core/artifact-graph/types.d.ts.map +1 -1
- package/dist/core/artifact-graph/types.js +30 -10
- package/dist/core/artifact-graph/types.js.map +1 -1
- package/dist/core/available-tools.d.ts +3 -12
- package/dist/core/available-tools.d.ts.map +1 -1
- package/dist/core/available-tools.js +4 -13
- package/dist/core/available-tools.js.map +1 -1
- package/dist/core/change-metadata/schema.d.ts +1 -1
- package/dist/core/change-metadata/schema.d.ts.map +1 -1
- package/dist/core/change-metadata/schema.js +10 -7
- package/dist/core/change-metadata/schema.js.map +1 -1
- package/dist/core/change-presenter.d.ts +16 -27
- package/dist/core/change-presenter.d.ts.map +1 -1
- package/dist/core/change-presenter.js +53 -53
- package/dist/core/change-presenter.js.map +1 -1
- package/dist/core/change-status-policy.d.ts +4 -8
- package/dist/core/change-status-policy.d.ts.map +1 -1
- package/dist/core/change-status-policy.js +9 -17
- package/dist/core/change-status-policy.js.map +1 -1
- package/dist/core/codex-metrics.d.ts +25 -45
- package/dist/core/codex-metrics.d.ts.map +1 -1
- package/dist/core/codex-metrics.js +44 -88
- package/dist/core/codex-metrics.js.map +1 -1
- package/dist/core/codex-residue.d.ts +14 -15
- package/dist/core/codex-residue.d.ts.map +1 -1
- package/dist/core/codex-residue.js +18 -22
- package/dist/core/codex-residue.js.map +1 -1
- package/dist/core/command-generation/adapters/claude.d.ts +2 -9
- package/dist/core/command-generation/adapters/claude.d.ts.map +1 -1
- package/dist/core/command-generation/adapters/claude.js +2 -12
- package/dist/core/command-generation/adapters/claude.js.map +1 -1
- package/dist/core/command-generation/adapters/index.d.ts +1 -9
- package/dist/core/command-generation/adapters/index.d.ts.map +1 -1
- package/dist/core/command-generation/adapters/index.js +1 -9
- package/dist/core/command-generation/adapters/index.js.map +1 -1
- package/dist/core/command-generation/generator.d.ts +0 -17
- package/dist/core/command-generation/generator.d.ts.map +1 -1
- package/dist/core/command-generation/generator.js +0 -17
- package/dist/core/command-generation/generator.js.map +1 -1
- package/dist/core/command-generation/index.d.ts +2 -5
- package/dist/core/command-generation/index.d.ts.map +1 -1
- package/dist/core/command-generation/index.js +0 -9
- package/dist/core/command-generation/index.js.map +1 -1
- package/dist/core/command-generation/types.d.ts +10 -36
- package/dist/core/command-generation/types.d.ts.map +1 -1
- package/dist/core/command-generation/types.js +0 -6
- package/dist/core/command-generation/types.js.map +1 -1
- package/dist/core/command-generation/yaml.d.ts +3 -18
- package/dist/core/command-generation/yaml.d.ts.map +1 -1
- package/dist/core/command-generation/yaml.js +5 -23
- package/dist/core/command-generation/yaml.js.map +1 -1
- package/dist/core/config-prompts.d.ts +2 -4
- package/dist/core/config-prompts.d.ts.map +1 -1
- package/dist/core/config-prompts.js +2 -7
- package/dist/core/config-prompts.js.map +1 -1
- package/dist/core/config-schema.d.ts +7 -41
- package/dist/core/config-schema.d.ts.map +1 -1
- package/dist/core/config-schema.js +35 -74
- package/dist/core/config-schema.js.map +1 -1
- package/dist/core/config.d.ts +25 -49
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +22 -45
- package/dist/core/config.js.map +1 -1
- package/dist/core/dashboard-activity.d.ts +7 -9
- package/dist/core/dashboard-activity.d.ts.map +1 -1
- package/dist/core/dashboard-activity.js +26 -24
- package/dist/core/dashboard-activity.js.map +1 -1
- package/dist/core/dashboard-data.d.ts +35 -22
- package/dist/core/dashboard-data.d.ts.map +1 -1
- package/dist/core/dashboard-data.js +57 -72
- package/dist/core/dashboard-data.js.map +1 -1
- package/dist/core/global-config.d.ts +24 -53
- package/dist/core/global-config.d.ts.map +1 -1
- package/dist/core/global-config.js +38 -67
- package/dist/core/global-config.js.map +1 -1
- package/dist/core/init.d.ts +12 -28
- package/dist/core/init.d.ts.map +1 -1
- package/dist/core/init.js +90 -168
- package/dist/core/init.js.map +1 -1
- package/dist/core/list.d.ts.map +1 -1
- package/dist/core/list.js +80 -38
- package/dist/core/list.js.map +1 -1
- package/dist/core/local-server.d.ts +41 -83
- package/dist/core/local-server.d.ts.map +1 -1
- package/dist/core/local-server.js +53 -98
- package/dist/core/local-server.js.map +1 -1
- package/dist/core/markdown-render.d.ts +15 -23
- package/dist/core/markdown-render.d.ts.map +1 -1
- package/dist/core/markdown-render.js +25 -34
- package/dist/core/markdown-render.js.map +1 -1
- package/dist/core/migrate.d.ts +19 -16
- package/dist/core/migrate.d.ts.map +1 -1
- package/dist/core/migrate.js +151 -135
- package/dist/core/migrate.js.map +1 -1
- package/dist/core/parsers/change-parser.d.ts +7 -10
- package/dist/core/parsers/change-parser.d.ts.map +1 -1
- package/dist/core/parsers/change-parser.js +48 -56
- package/dist/core/parsers/change-parser.js.map +1 -1
- package/dist/core/parsers/markdown-parser.d.ts +8 -9
- package/dist/core/parsers/markdown-parser.d.ts.map +1 -1
- package/dist/core/parsers/markdown-parser.js +23 -30
- package/dist/core/parsers/markdown-parser.js.map +1 -1
- package/dist/core/parsers/requirement-blocks.d.ts +43 -15
- package/dist/core/parsers/requirement-blocks.d.ts.map +1 -1
- package/dist/core/parsers/requirement-blocks.js +142 -70
- package/dist/core/parsers/requirement-blocks.js.map +1 -1
- package/dist/core/parsers/requirement-text.d.ts +73 -79
- package/dist/core/parsers/requirement-text.d.ts.map +1 -1
- package/dist/core/parsers/requirement-text.js +137 -79
- package/dist/core/parsers/requirement-text.js.map +1 -1
- package/dist/core/parsers/spec-structure.d.ts.map +1 -1
- package/dist/core/parsers/spec-structure.js +10 -6
- package/dist/core/parsers/spec-structure.js.map +1 -1
- package/dist/core/profiles.d.ts +3 -10
- package/dist/core/profiles.d.ts.map +1 -1
- package/dist/core/profiles.js +5 -12
- package/dist/core/profiles.js.map +1 -1
- package/dist/core/project-config.d.ts +43 -44
- package/dist/core/project-config.d.ts.map +1 -1
- package/dist/core/project-config.js +107 -82
- package/dist/core/project-config.js.map +1 -1
- package/dist/core/project-layout.d.ts +9 -17
- package/dist/core/project-layout.d.ts.map +1 -1
- package/dist/core/project-layout.js +16 -26
- package/dist/core/project-layout.js.map +1 -1
- package/dist/core/root-selection.d.ts +8 -14
- package/dist/core/root-selection.d.ts.map +1 -1
- package/dist/core/root-selection.js +3 -6
- package/dist/core/root-selection.js.map +1 -1
- package/dist/core/rules.d.ts.map +1 -1
- package/dist/core/rules.js +2 -3
- package/dist/core/rules.js.map +1 -1
- package/dist/core/schema-names.d.ts +16 -0
- package/dist/core/schema-names.d.ts.map +1 -0
- package/dist/core/schema-names.js +16 -0
- package/dist/core/schema-names.js.map +1 -0
- package/dist/core/schemas/base.schema.d.ts.map +1 -1
- package/dist/core/schemas/base.schema.js +6 -12
- package/dist/core/schemas/base.schema.js.map +1 -1
- package/dist/core/schemas/change.schema.d.ts +8 -0
- package/dist/core/schemas/change.schema.d.ts.map +1 -1
- package/dist/core/schemas/change.schema.js +41 -10
- package/dist/core/schemas/change.schema.js.map +1 -1
- package/dist/core/shared/index.d.ts +2 -7
- package/dist/core/shared/index.d.ts.map +1 -1
- package/dist/core/shared/index.js +2 -7
- package/dist/core/shared/index.js.map +1 -1
- package/dist/core/shared/rules-generation.d.ts +5 -15
- package/dist/core/shared/rules-generation.d.ts.map +1 -1
- package/dist/core/shared/rules-generation.js +33 -37
- package/dist/core/shared/rules-generation.js.map +1 -1
- package/dist/core/shared/skill-generation.d.ts +28 -43
- package/dist/core/shared/skill-generation.d.ts.map +1 -1
- package/dist/core/shared/skill-generation.js +82 -51
- package/dist/core/shared/skill-generation.js.map +1 -1
- package/dist/core/shared/tool-detection.d.ts +35 -76
- package/dist/core/shared/tool-detection.d.ts.map +1 -1
- package/dist/core/shared/tool-detection.js +73 -93
- package/dist/core/shared/tool-detection.js.map +1 -1
- package/dist/core/skill-metrics.d.ts +36 -63
- package/dist/core/skill-metrics.d.ts.map +1 -1
- package/dist/core/skill-metrics.js +34 -73
- package/dist/core/skill-metrics.js.map +1 -1
- package/dist/core/spec-presenter.d.ts.map +1 -1
- package/dist/core/spec-presenter.js +5 -10
- package/dist/core/spec-presenter.js.map +1 -1
- package/dist/core/specs-apply.d.ts +16 -31
- package/dist/core/specs-apply.d.ts.map +1 -1
- package/dist/core/specs-apply.js +146 -195
- package/dist/core/specs-apply.js.map +1 -1
- package/dist/core/templates/fragments/interview.d.ts +2 -6
- package/dist/core/templates/fragments/interview.d.ts.map +1 -1
- package/dist/core/templates/fragments/interview.js +2 -6
- package/dist/core/templates/fragments/interview.js.map +1 -1
- package/dist/core/templates/fragments/next-step.d.ts +4 -8
- package/dist/core/templates/fragments/next-step.d.ts.map +1 -1
- package/dist/core/templates/fragments/next-step.js +4 -8
- package/dist/core/templates/fragments/next-step.js.map +1 -1
- package/dist/core/templates/fragments/validate.d.ts +13 -0
- package/dist/core/templates/fragments/validate.d.ts.map +1 -0
- package/dist/core/templates/fragments/validate.js +13 -0
- package/dist/core/templates/fragments/validate.js.map +1 -0
- package/dist/core/templates/fragments/verify.d.ts +9 -12
- package/dist/core/templates/fragments/verify.d.ts.map +1 -1
- package/dist/core/templates/fragments/verify.js +9 -12
- package/dist/core/templates/fragments/verify.js.map +1 -1
- package/dist/core/templates/index.d.ts +0 -6
- package/dist/core/templates/index.d.ts.map +1 -1
- package/dist/core/templates/index.js +0 -7
- package/dist/core/templates/index.js.map +1 -1
- package/dist/core/templates/skill-templates.d.ts +1 -5
- package/dist/core/templates/skill-templates.d.ts.map +1 -1
- package/dist/core/templates/skill-templates.js +0 -5
- package/dist/core/templates/skill-templates.js.map +1 -1
- package/dist/core/templates/types.d.ts +3 -7
- package/dist/core/templates/types.d.ts.map +1 -1
- package/dist/core/templates/types.js +0 -3
- package/dist/core/templates/types.js.map +1 -1
- package/dist/core/templates/workflows/apply.d.ts +3 -9
- package/dist/core/templates/workflows/apply.d.ts.map +1 -1
- package/dist/core/templates/workflows/apply.js +11 -13
- package/dist/core/templates/workflows/apply.js.map +1 -1
- package/dist/core/templates/workflows/archive.d.ts +0 -6
- package/dist/core/templates/workflows/archive.d.ts.map +1 -1
- package/dist/core/templates/workflows/archive.js +16 -5
- package/dist/core/templates/workflows/archive.js.map +1 -1
- package/dist/core/templates/workflows/decision.js +3 -3
- package/dist/core/templates/workflows/decision.js.map +1 -1
- package/dist/core/templates/workflows/explore.js +1 -1
- package/dist/core/templates/workflows/grill.d.ts.map +1 -1
- package/dist/core/templates/workflows/grill.js +0 -2
- package/dist/core/templates/workflows/grill.js.map +1 -1
- package/dist/core/templates/workflows/issue.d.ts +0 -6
- package/dist/core/templates/workflows/issue.d.ts.map +1 -1
- package/dist/core/templates/workflows/issue.js +3 -2
- package/dist/core/templates/workflows/issue.js.map +1 -1
- package/dist/core/templates/workflows/propose.d.ts +0 -6
- package/dist/core/templates/workflows/propose.d.ts.map +1 -1
- package/dist/core/templates/workflows/propose.js +2 -2
- package/dist/core/templates/workflows/propose.js.map +1 -1
- package/dist/core/templates/workflows/sync.d.ts +2 -8
- package/dist/core/templates/workflows/sync.d.ts.map +1 -1
- package/dist/core/templates/workflows/sync.js +4 -3
- package/dist/core/templates/workflows/sync.js.map +1 -1
- package/dist/core/templates/workflows/update.d.ts +0 -6
- package/dist/core/templates/workflows/update.d.ts.map +1 -1
- package/dist/core/templates/workflows/update.js +9 -2
- package/dist/core/templates/workflows/update.js.map +1 -1
- package/dist/core/update.d.ts +10 -36
- package/dist/core/update.d.ts.map +1 -1
- package/dist/core/update.js +59 -126
- package/dist/core/update.js.map +1 -1
- package/dist/core/user-state-migration.d.ts +13 -15
- package/dist/core/user-state-migration.d.ts.map +1 -1
- package/dist/core/user-state-migration.js +16 -20
- package/dist/core/user-state-migration.js.map +1 -1
- package/dist/core/validation/constants.d.ts +4 -10
- package/dist/core/validation/constants.d.ts.map +1 -1
- package/dist/core/validation/constants.js +25 -25
- package/dist/core/validation/constants.js.map +1 -1
- package/dist/core/validation/prose-length.d.ts +15 -0
- package/dist/core/validation/prose-length.d.ts.map +1 -0
- package/dist/core/validation/prose-length.js +29 -0
- package/dist/core/validation/prose-length.js.map +1 -0
- package/dist/core/validation/purpose-placeholder.d.ts +9 -16
- package/dist/core/validation/purpose-placeholder.d.ts.map +1 -1
- package/dist/core/validation/purpose-placeholder.js +30 -44
- package/dist/core/validation/purpose-placeholder.js.map +1 -1
- package/dist/core/validation/section-validator.d.ts +4 -4
- package/dist/core/validation/section-validator.d.ts.map +1 -1
- package/dist/core/validation/section-validator.js +30 -12
- package/dist/core/validation/section-validator.js.map +1 -1
- package/dist/core/validation/task-numbering.d.ts +6 -3
- package/dist/core/validation/task-numbering.d.ts.map +1 -1
- package/dist/core/validation/task-numbering.js +23 -11
- package/dist/core/validation/task-numbering.js.map +1 -1
- package/dist/core/validation/types.d.ts +18 -0
- package/dist/core/validation/types.d.ts.map +1 -1
- package/dist/core/validation/types.js +12 -1
- package/dist/core/validation/types.js.map +1 -1
- package/dist/core/validation/validator.d.ts +42 -68
- package/dist/core/validation/validator.d.ts.map +1 -1
- package/dist/core/validation/validator.js +468 -285
- package/dist/core/validation/validator.js.map +1 -1
- package/dist/prompts/searchable-multi-select.d.ts +3 -8
- package/dist/prompts/searchable-multi-select.d.ts.map +1 -1
- package/dist/prompts/searchable-multi-select.js +16 -39
- package/dist/prompts/searchable-multi-select.js.map +1 -1
- package/dist/utils/change-metadata.d.ts +11 -50
- package/dist/utils/change-metadata.d.ts.map +1 -1
- package/dist/utils/change-metadata.js +48 -67
- package/dist/utils/change-metadata.js.map +1 -1
- package/dist/utils/change-utils.d.ts +24 -76
- package/dist/utils/change-utils.d.ts.map +1 -1
- package/dist/utils/change-utils.js +95 -142
- package/dist/utils/change-utils.js.map +1 -1
- package/dist/utils/file-lock.d.ts +39 -0
- package/dist/utils/file-lock.d.ts.map +1 -0
- package/dist/utils/file-lock.js +149 -0
- package/dist/utils/file-lock.js.map +1 -0
- package/dist/utils/file-system.d.ts +12 -32
- package/dist/utils/file-system.d.ts.map +1 -1
- package/dist/utils/file-system.js +16 -40
- package/dist/utils/file-system.js.map +1 -1
- package/dist/utils/frontmatter.d.ts +7 -11
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +11 -11
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/interactive.d.ts +4 -9
- package/dist/utils/interactive.d.ts.map +1 -1
- package/dist/utils/interactive.js +2 -4
- package/dist/utils/interactive.js.map +1 -1
- package/dist/utils/item-discovery.d.ts +10 -20
- package/dist/utils/item-discovery.d.ts.map +1 -1
- package/dist/utils/item-discovery.js +31 -55
- package/dist/utils/item-discovery.js.map +1 -1
- package/dist/utils/link.d.ts +9 -18
- package/dist/utils/link.d.ts.map +1 -1
- package/dist/utils/link.js +9 -18
- package/dist/utils/link.js.map +1 -1
- package/dist/utils/requirement-diff.d.ts +13 -23
- package/dist/utils/requirement-diff.d.ts.map +1 -1
- package/dist/utils/requirement-diff.js +13 -23
- package/dist/utils/requirement-diff.js.map +1 -1
- package/dist/utils/spec-files.d.ts +7 -20
- package/dist/utils/spec-files.d.ts.map +1 -1
- package/dist/utils/spec-files.js +26 -51
- package/dist/utils/spec-files.js.map +1 -1
- package/dist/utils/task-progress.d.ts +11 -9
- package/dist/utils/task-progress.d.ts.map +1 -1
- package/dist/utils/task-progress.js +53 -32
- package/dist/utils/task-progress.js.map +1 -1
- package/dist/utils/timestamp.d.ts +5 -8
- package/dist/utils/timestamp.d.ts.map +1 -1
- package/dist/utils/timestamp.js +5 -8
- package/dist/utils/timestamp.js.map +1 -1
- package/package.json +2 -3
- package/schemas/issue/schema.yaml +8 -1
- package/schemas/issue/templates/spec.md +20 -3
- package/schemas/sdd/schema.yaml +20 -1
- package/schemas/sdd/templates/spec.md +20 -3
|
@@ -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';
|
|
@@ -24,20 +16,13 @@ import { loadTemplate } from '../core/artifact-graph/index.js';
|
|
|
24
16
|
import { MarkdownParser, findSection } from '../core/parsers/markdown-parser.js';
|
|
25
17
|
import { completeRootStructure, validateChangeName } from '../utils/change-utils.js';
|
|
26
18
|
import { formatTimestamp } from '../utils/timestamp.js';
|
|
27
|
-
import { DEFAULT_SCHEMA } from '
|
|
19
|
+
import { DEFAULT_SCHEMA, DECISION_SCHEMA_NAME } from '../core/schema-names.js';
|
|
28
20
|
import { emitSuccess, emitFailure } from './shared-output.js';
|
|
29
|
-
|
|
30
|
-
// Types
|
|
31
|
-
// -----------------------------------------------------------------------------
|
|
21
|
+
import { stripBom } from '../core/parsers/requirement-text.js';
|
|
32
22
|
const DECISION_STATUSES = ['proposed', 'accepted', 'superseded'];
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
* `tospec/decisions/`, so a change created against it produced a directory
|
|
37
|
-
* under `tospec/changes/` that `decision list` never saw, with no ticket and
|
|
38
|
-
* no `type`.
|
|
39
|
-
*/
|
|
40
|
-
export const DECISION_SCHEMA_NAME = 'decision';
|
|
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 };
|
|
41
26
|
/** The `--json` null-shapes; shared by the commander table and the catch sites. */
|
|
42
27
|
export const DECISION_NEW_FAILURE_PAYLOAD = {
|
|
43
28
|
decision: null,
|
|
@@ -51,18 +36,14 @@ const SCHEMA_NAME = DECISION_SCHEMA_NAME;
|
|
|
51
36
|
const DECISIONS_SUBDIR = path.join('tospec', 'decisions');
|
|
52
37
|
// Aligns with the project-wide CLI timestamp (formatTimestamp): yyyyMMdd_HHmmss.
|
|
53
38
|
const TIMESTAMP_RE = /^\d{8}_\d{6}$/;
|
|
54
|
-
/** The same shape, split into fields for calendar checking and display. */
|
|
55
39
|
const TIMESTAMP_PARTS_RE = /^(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})$/;
|
|
56
40
|
const FILE_RE = /^(\d{8}_\d{6})-(.+)\.md$/;
|
|
57
|
-
// -----------------------------------------------------------------------------
|
|
58
|
-
// Helpers
|
|
59
|
-
// -----------------------------------------------------------------------------
|
|
60
41
|
/** Title = the H1 heading; falls back to the topic derived from the filename. */
|
|
61
42
|
function parseTitle(sections, fallback) {
|
|
62
43
|
const h1 = sections.find((s) => s.level === 1);
|
|
63
44
|
return h1?.title.trim() || fallback;
|
|
64
45
|
}
|
|
65
|
-
/** Status = the first ASCII-word bullet under
|
|
46
|
+
/** Status = the first ASCII-word bullet under `Status` (the `Date:` bullet follows it). */
|
|
66
47
|
function parseStatus(sections) {
|
|
67
48
|
const statusSection = findSection(sections, 'Status');
|
|
68
49
|
if (!statusSection)
|
|
@@ -105,28 +86,27 @@ function renderDecision(template, title, stamp, status) {
|
|
|
105
86
|
.replace('[yyyyMMdd_HHmmss]', stamp)
|
|
106
87
|
.replace(/^- proposed$/m, `- ${status}`);
|
|
107
88
|
}
|
|
108
|
-
/** `20260722_161825` -> `2026-07-22 16:18:25` for the index row (falls back to the raw stamp). */
|
|
109
89
|
/**
|
|
110
90
|
* Whether a shape-valid stamp also names a real instant.
|
|
111
91
|
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* by, where it outranks every real record.
|
|
117
|
-
*
|
|
118
|
-
* Round-tripped rather than range-checked per field, so month lengths and leap
|
|
119
|
-
* days come from the calendar instead of a second implementation of it.
|
|
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.
|
|
120
96
|
*/
|
|
121
97
|
function isRealTimestamp(stamp) {
|
|
122
98
|
const m = TIMESTAMP_PARTS_RE.exec(stamp);
|
|
123
99
|
if (!m)
|
|
124
100
|
return false;
|
|
125
101
|
const [, y, mo, d, h, mi, sec] = m.map(Number);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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);
|
|
129
108
|
}
|
|
109
|
+
/** `20260722_161825` -> `2026-07-22 16:18:25`, or the raw stamp if unparseable. */
|
|
130
110
|
function stampToHuman(stamp) {
|
|
131
111
|
const m = TIMESTAMP_PARTS_RE.exec(stamp);
|
|
132
112
|
if (!m)
|
|
@@ -148,11 +128,9 @@ function ensureIndex(decisionsDir, projectRoot) {
|
|
|
148
128
|
}
|
|
149
129
|
return indexPath;
|
|
150
130
|
}
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
// `file` is the sibling filename (index.md lives in the same decisions dir), so
|
|
155
|
-
// a relative markdown link resolves straight to the decision doc.
|
|
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.
|
|
156
134
|
function indexRow(timestamp, status, title, summary, file) {
|
|
157
135
|
const fileLink = `[${indexCell(file)}](${file})`;
|
|
158
136
|
return `| ${timestamp} | ${indexCell(status)} | ${indexCell(title)} | ${indexCell(summary)} | ${fileLink} |`;
|
|
@@ -161,19 +139,12 @@ function indexRow(timestamp, status, title, summary, file) {
|
|
|
161
139
|
const INDEX_HEADER = '| timestamp | status | title | summary | file |';
|
|
162
140
|
const INDEX_SEPARATOR = '| ----- | ----- | ----- | ----- | ----- |';
|
|
163
141
|
/**
|
|
164
|
-
* Brings a pre-status ledger up to the current five-column shape.
|
|
165
|
-
*
|
|
166
|
-
* Round 4 report 14: `index.md` recorded timestamp/title/summary/file, so a
|
|
167
|
-
* superseded decision was indistinguishable from a live one in the file people
|
|
168
|
-
* and agents actually read — the decision skill even tells the author to flip an
|
|
169
|
-
* older record's status, and nothing about the ledger changed when they did.
|
|
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.
|
|
170
144
|
*
|
|
171
|
-
* Each
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
* that `decision list` reports for the same case. Everything else in the row is
|
|
175
|
-
* preserved — this is a column insert, not a rebuild, so hand-written cells
|
|
176
|
-
* survive.
|
|
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.
|
|
177
148
|
*/
|
|
178
149
|
function upgradeIndexColumns(lines, decisionsDir) {
|
|
179
150
|
return lines.map((line) => {
|
|
@@ -192,6 +163,39 @@ function upgradeIndexColumns(lines, decisionsDir) {
|
|
|
192
163
|
return `|${cells[0]}| ${status} |${cells[1]}|${cells[2]}|${cells[3]}|`;
|
|
193
164
|
});
|
|
194
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
|
+
}
|
|
195
199
|
/** The status recorded in a decision file, or `unknown` when it cannot be read. */
|
|
196
200
|
function statusOfDecisionFile(decisionsDir, file) {
|
|
197
201
|
try {
|
|
@@ -203,47 +207,164 @@ function statusOfDecisionFile(decisionsDir, file) {
|
|
|
203
207
|
}
|
|
204
208
|
}
|
|
205
209
|
/**
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
* leave the old one either. Skipping the write entirely was the previous
|
|
210
|
-
* behaviour, and it let `--force` change a decision's title and status in the
|
|
211
|
-
* file while the ledger kept advertising the old ones: `decision list` reads the
|
|
212
|
-
* files and index.md is read by people, so the two silently disagreed with no
|
|
213
|
-
* way to tell which was current.
|
|
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.
|
|
214
213
|
*
|
|
215
|
-
* Matched on the filename
|
|
216
|
-
* what a rewrite is likely to change.
|
|
214
|
+
* Matched on the filename, not the title — the title is what a rewrite changes.
|
|
217
215
|
*/
|
|
218
216
|
function writeIndexRow(indexPath, timestamp, status, title, summary, file) {
|
|
219
217
|
const row = indexRow(timestamp, status, title, summary, file);
|
|
220
|
-
|
|
221
|
-
//
|
|
222
|
-
|
|
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.
|
|
223
222
|
const eol = existing.includes('\r\n') ? '\r\n' : '\n';
|
|
224
223
|
let lines = existing.split(/\r?\n/);
|
|
225
|
-
// A ledger written before the status column gets it here,
|
|
226
|
-
//
|
|
224
|
+
// A ledger written before the status column gets it here, rather than growing
|
|
225
|
+
// a row one cell wider than its header.
|
|
227
226
|
if (!existing.includes('| status |')) {
|
|
228
227
|
lines = upgradeIndexColumns(lines, path.dirname(indexPath));
|
|
229
|
-
existing = lines.join(eol);
|
|
230
|
-
fs.writeFileSync(indexPath, existing, 'utf-8');
|
|
231
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
232
|
const linkTarget = `](${file})`;
|
|
233
233
|
const index = lines.findIndex((line) => line.startsWith('|') && line.includes(linkTarget));
|
|
234
234
|
if (index === -1) {
|
|
235
|
-
const
|
|
236
|
-
|
|
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');
|
|
237
238
|
return;
|
|
238
239
|
}
|
|
239
240
|
lines[index] = row;
|
|
240
241
|
fs.writeFileSync(indexPath, lines.join(eol), 'utf-8');
|
|
241
242
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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 };
|
|
348
|
+
}
|
|
245
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
|
+
});
|
|
246
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;
|
|
247
368
|
if (!topic)
|
|
248
369
|
throw new Error('Missing required argument <topic>');
|
|
249
370
|
const nameValidation = validateChangeName(topic, 'Topic');
|
|
@@ -261,12 +382,6 @@ export async function decisionNewCommand(topic, options) {
|
|
|
261
382
|
if (!isRealTimestamp(stamp)) {
|
|
262
383
|
throw new Error(`Invalid --date '${stamp}'. That is not a real date and time.`);
|
|
263
384
|
}
|
|
264
|
-
const root = await resolveRootForCommand(options, {
|
|
265
|
-
json: options.json,
|
|
266
|
-
failurePayload: DECISION_NEW_FAILURE_PAYLOAD,
|
|
267
|
-
});
|
|
268
|
-
if (!root)
|
|
269
|
-
return;
|
|
270
385
|
const decisionsDir = path.join(root.path, DECISIONS_SUBDIR);
|
|
271
386
|
const fileName = `${stamp}-${topic}.md`;
|
|
272
387
|
const filePath = path.join(decisionsDir, fileName);
|
|
@@ -274,24 +389,19 @@ export async function decisionNewCommand(topic, options) {
|
|
|
274
389
|
const summary = options.summary ?? title;
|
|
275
390
|
const template = loadTemplate(SCHEMA_NAME, 'decision.md', root.path);
|
|
276
391
|
const content = renderDecision(template, title, stamp, status);
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
// one subdirectory it needs left a half-root that every other command then
|
|
283
|
-
// resolved as a project, making `validate --all` and `status --all` report a
|
|
284
|
-
// clean sweep over somewhere that had never been one. `new change` has always
|
|
285
|
-
// completed the root for this reason; they now share the one implementation.
|
|
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.
|
|
286
397
|
await completeRootStructure(root.path, DEFAULT_SCHEMA);
|
|
287
398
|
fs.mkdirSync(decisionsDir, { recursive: true });
|
|
288
399
|
if (options.force) {
|
|
289
400
|
fs.writeFileSync(filePath, content, 'utf-8');
|
|
290
401
|
}
|
|
291
402
|
else {
|
|
292
|
-
// 'wx' makes the create atomic
|
|
293
|
-
//
|
|
294
|
-
// 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.
|
|
295
405
|
try {
|
|
296
406
|
fs.writeFileSync(filePath, content, { encoding: 'utf-8', flag: 'wx' });
|
|
297
407
|
}
|
|
@@ -303,9 +413,8 @@ export async function decisionNewCommand(topic, options) {
|
|
|
303
413
|
}
|
|
304
414
|
}
|
|
305
415
|
const indexPath = ensureIndex(decisionsDir, root.path);
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
// rather than leave the previous ones standing.
|
|
416
|
+
// Unconditional: a --force that rewrote the file has to carry its new title
|
|
417
|
+
// and summary into the ledger.
|
|
309
418
|
writeIndexRow(indexPath, stampToHuman(stamp), status, title, summary, fileName);
|
|
310
419
|
const payload = {
|
|
311
420
|
decision: { topic, title, date: stamp, status, summary, path: filePath, indexPath },
|
|
@@ -322,7 +431,7 @@ export async function decisionNewCommand(topic, options) {
|
|
|
322
431
|
}
|
|
323
432
|
catch (error) {
|
|
324
433
|
if (options.json) {
|
|
325
|
-
emitFailure(true,
|
|
434
|
+
emitFailure(true, failurePayload(), error, 'decision_error');
|
|
326
435
|
return;
|
|
327
436
|
}
|
|
328
437
|
throw error;
|
|
@@ -330,11 +439,9 @@ export async function decisionNewCommand(topic, options) {
|
|
|
330
439
|
}
|
|
331
440
|
export async function decisionListCommand(options) {
|
|
332
441
|
try {
|
|
333
|
-
// No implicit root,
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
// never examined. A named lookup cannot fail that way, which is why `show` and
|
|
337
|
-
// `validate <item>` still accept an implicit root; a listing can.
|
|
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.
|
|
338
445
|
const root = await resolveRootForCommand(options, {
|
|
339
446
|
json: options.json,
|
|
340
447
|
allowImplicitRoot: false,
|
|
@@ -344,6 +451,24 @@ export async function decisionListCommand(options) {
|
|
|
344
451
|
return;
|
|
345
452
|
const decisionsDir = path.join(root.path, DECISIONS_SUBDIR);
|
|
346
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
|
+
}
|
|
347
472
|
if (options.status) {
|
|
348
473
|
entries = entries.filter((e) => e.status === options.status);
|
|
349
474
|
}
|
|
@@ -352,20 +477,69 @@ export async function decisionListCommand(options) {
|
|
|
352
477
|
entries.sort((a, b) => a.topic.localeCompare(b.topic));
|
|
353
478
|
}
|
|
354
479
|
else {
|
|
355
|
-
//
|
|
480
|
+
// Newest first; tie-break by topic for stability.
|
|
356
481
|
entries.sort((a, b) => b.date.localeCompare(a.date) || a.topic.localeCompare(b.topic));
|
|
357
482
|
}
|
|
358
483
|
if (options.json) {
|
|
359
|
-
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));
|
|
360
502
|
return;
|
|
361
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
|
+
}
|
|
362
535
|
if (entries.length === 0) {
|
|
363
|
-
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>');
|
|
364
539
|
return;
|
|
365
540
|
}
|
|
366
|
-
// Widths derived from the data
|
|
367
|
-
//
|
|
368
|
-
// with the rule beneath it.
|
|
541
|
+
// Widths derived from the data: a hardcoded width never lined a
|
|
542
|
+
// `yyyyMMdd_HHmmss` stamp up with the rule beneath it.
|
|
369
543
|
const dateWidth = Math.max('Date'.length, ...entries.map((e) => e.date.length));
|
|
370
544
|
const statusWidth = Math.max('Status'.length, ...entries.map((e) => e.status.length));
|
|
371
545
|
console.log(`${'Date'.padEnd(dateWidth)} | ${'Status'.padEnd(statusWidth)} | Title`);
|
|
@@ -382,14 +556,6 @@ export async function decisionListCommand(options) {
|
|
|
382
556
|
throw error;
|
|
383
557
|
}
|
|
384
558
|
}
|
|
385
|
-
// -----------------------------------------------------------------------------
|
|
386
|
-
// Registration
|
|
387
|
-
// -----------------------------------------------------------------------------
|
|
388
|
-
/**
|
|
389
|
-
* Register the `decision` command group and its subcommands.
|
|
390
|
-
*
|
|
391
|
-
* @param program - The Commander program instance
|
|
392
|
-
*/
|
|
393
559
|
export function registerDecisionCommand(program) {
|
|
394
560
|
const decisionCmd = program
|
|
395
561
|
.command('decision')
|
|
@@ -408,8 +574,10 @@ export function registerDecisionCommand(program) {
|
|
|
408
574
|
await decisionNewCommand(topic, options);
|
|
409
575
|
}
|
|
410
576
|
catch (error) {
|
|
411
|
-
|
|
412
|
-
|
|
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');
|
|
413
581
|
}
|
|
414
582
|
});
|
|
415
583
|
decisionCmd
|
|
@@ -417,14 +585,14 @@ export function registerDecisionCommand(program) {
|
|
|
417
585
|
.description('List decisions as an index table')
|
|
418
586
|
.addOption(new Option('--status <status>', 'Filter by status').choices(DECISION_STATUSES))
|
|
419
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")
|
|
420
589
|
.option('--json', 'Output as JSON')
|
|
421
590
|
.action(async (options) => {
|
|
422
591
|
try {
|
|
423
592
|
await decisionListCommand(options);
|
|
424
593
|
}
|
|
425
594
|
catch (error) {
|
|
426
|
-
|
|
427
|
-
process.exitCode = 1;
|
|
595
|
+
emitFailure(false, {}, error, 'decision_error');
|
|
428
596
|
}
|
|
429
597
|
});
|
|
430
598
|
}
|