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