@seanmars/tospec 0.19.0-beta.7 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -231
- package/README.md +69 -82
- package/assets/dashboard/app.js +14 -3
- package/assets/dashboard/style.css +7 -0
- package/assets/rules/tospec/decision.md +3 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +227 -86
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/config.d.ts +9 -17
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +366 -107
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/dashboard.d.ts +57 -99
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dashboard.js +224 -282
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/decision.d.ts +41 -19
- package/dist/commands/decision.d.ts.map +1 -1
- package/dist/commands/decision.js +385 -79
- package/dist/commands/decision.js.map +1 -1
- package/dist/commands/metrics.d.ts +28 -51
- package/dist/commands/metrics.d.ts.map +1 -1
- package/dist/commands/metrics.js +62 -93
- package/dist/commands/metrics.js.map +1 -1
- package/dist/commands/shared-output.d.ts +18 -21
- package/dist/commands/shared-output.d.ts.map +1 -1
- package/dist/commands/shared-output.js +49 -23
- package/dist/commands/shared-output.js.map +1 -1
- package/dist/commands/show.d.ts +7 -0
- package/dist/commands/show.d.ts.map +1 -1
- package/dist/commands/show.js +29 -7
- package/dist/commands/show.js.map +1 -1
- package/dist/commands/validate.d.ts +43 -45
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +262 -129
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/workflow/index.d.ts +6 -10
- package/dist/commands/workflow/index.d.ts.map +1 -1
- package/dist/commands/workflow/index.js +6 -10
- package/dist/commands/workflow/index.js.map +1 -1
- package/dist/commands/workflow/instructions.d.ts +21 -8
- package/dist/commands/workflow/instructions.d.ts.map +1 -1
- package/dist/commands/workflow/instructions.js +254 -95
- package/dist/commands/workflow/instructions.js.map +1 -1
- package/dist/commands/workflow/new-change.d.ts +4 -5
- package/dist/commands/workflow/new-change.d.ts.map +1 -1
- package/dist/commands/workflow/new-change.js +90 -25
- package/dist/commands/workflow/new-change.js.map +1 -1
- package/dist/commands/workflow/schemas.d.ts +3 -5
- package/dist/commands/workflow/schemas.d.ts.map +1 -1
- package/dist/commands/workflow/schemas.js +37 -11
- package/dist/commands/workflow/schemas.js.map +1 -1
- package/dist/commands/workflow/shared.d.ts +48 -21
- package/dist/commands/workflow/shared.d.ts.map +1 -1
- package/dist/commands/workflow/shared.js +36 -33
- package/dist/commands/workflow/shared.js.map +1 -1
- package/dist/commands/workflow/status.d.ts +10 -6
- package/dist/commands/workflow/status.d.ts.map +1 -1
- package/dist/commands/workflow/status.js +81 -42
- package/dist/commands/workflow/status.js.map +1 -1
- package/dist/commands/workflow/templates.d.ts +10 -3
- package/dist/commands/workflow/templates.d.ts.map +1 -1
- package/dist/commands/workflow/templates.js +39 -40
- package/dist/commands/workflow/templates.js.map +1 -1
- package/dist/core/archive.d.ts +26 -21
- package/dist/core/archive.d.ts.map +1 -1
- package/dist/core/archive.js +378 -235
- package/dist/core/archive.js.map +1 -1
- package/dist/core/artifact-graph/graph.d.ts +25 -42
- package/dist/core/artifact-graph/graph.d.ts.map +1 -1
- package/dist/core/artifact-graph/graph.js +45 -63
- package/dist/core/artifact-graph/graph.js.map +1 -1
- package/dist/core/artifact-graph/index.d.ts +1 -1
- package/dist/core/artifact-graph/index.d.ts.map +1 -1
- package/dist/core/artifact-graph/index.js +1 -1
- package/dist/core/artifact-graph/index.js.map +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +97 -103
- package/dist/core/artifact-graph/instruction-loader.d.ts.map +1 -1
- package/dist/core/artifact-graph/instruction-loader.js +146 -119
- package/dist/core/artifact-graph/instruction-loader.js.map +1 -1
- package/dist/core/artifact-graph/outputs.d.ts +9 -23
- package/dist/core/artifact-graph/outputs.d.ts.map +1 -1
- package/dist/core/artifact-graph/outputs.js +45 -38
- package/dist/core/artifact-graph/outputs.js.map +1 -1
- package/dist/core/artifact-graph/resolver.d.ts +44 -63
- package/dist/core/artifact-graph/resolver.d.ts.map +1 -1
- package/dist/core/artifact-graph/resolver.js +85 -86
- package/dist/core/artifact-graph/resolver.js.map +1 -1
- package/dist/core/artifact-graph/schema.d.ts +0 -6
- package/dist/core/artifact-graph/schema.d.ts.map +1 -1
- package/dist/core/artifact-graph/schema.js +7 -32
- package/dist/core/artifact-graph/schema.js.map +1 -1
- package/dist/core/artifact-graph/state.d.ts +1 -8
- package/dist/core/artifact-graph/state.d.ts.map +1 -1
- package/dist/core/artifact-graph/state.js +2 -17
- package/dist/core/artifact-graph/state.js.map +1 -1
- package/dist/core/artifact-graph/stub-detection.d.ts +12 -0
- package/dist/core/artifact-graph/stub-detection.d.ts.map +1 -0
- package/dist/core/artifact-graph/stub-detection.js +39 -0
- package/dist/core/artifact-graph/stub-detection.js.map +1 -0
- package/dist/core/artifact-graph/types.d.ts +4 -0
- package/dist/core/artifact-graph/types.d.ts.map +1 -1
- package/dist/core/artifact-graph/types.js +30 -10
- package/dist/core/artifact-graph/types.js.map +1 -1
- package/dist/core/available-tools.d.ts +3 -12
- package/dist/core/available-tools.d.ts.map +1 -1
- package/dist/core/available-tools.js +4 -13
- package/dist/core/available-tools.js.map +1 -1
- package/dist/core/change-metadata/schema.d.ts +1 -1
- package/dist/core/change-metadata/schema.d.ts.map +1 -1
- package/dist/core/change-metadata/schema.js +10 -7
- package/dist/core/change-metadata/schema.js.map +1 -1
- package/dist/core/change-presenter.d.ts +23 -18
- package/dist/core/change-presenter.d.ts.map +1 -1
- package/dist/core/change-presenter.js +102 -43
- package/dist/core/change-presenter.js.map +1 -1
- package/dist/core/change-status-policy.d.ts +8 -1
- package/dist/core/change-status-policy.d.ts.map +1 -1
- package/dist/core/change-status-policy.js +25 -1
- package/dist/core/change-status-policy.js.map +1 -1
- package/dist/core/codex-metrics.d.ts +25 -45
- package/dist/core/codex-metrics.d.ts.map +1 -1
- package/dist/core/codex-metrics.js +44 -88
- package/dist/core/codex-metrics.js.map +1 -1
- package/dist/core/codex-residue.d.ts +14 -15
- package/dist/core/codex-residue.d.ts.map +1 -1
- package/dist/core/codex-residue.js +18 -22
- package/dist/core/codex-residue.js.map +1 -1
- package/dist/core/command-generation/adapters/claude.d.ts +2 -9
- package/dist/core/command-generation/adapters/claude.d.ts.map +1 -1
- package/dist/core/command-generation/adapters/claude.js +2 -12
- package/dist/core/command-generation/adapters/claude.js.map +1 -1
- package/dist/core/command-generation/adapters/index.d.ts +1 -9
- package/dist/core/command-generation/adapters/index.d.ts.map +1 -1
- package/dist/core/command-generation/adapters/index.js +1 -9
- package/dist/core/command-generation/adapters/index.js.map +1 -1
- package/dist/core/command-generation/generator.d.ts +0 -17
- package/dist/core/command-generation/generator.d.ts.map +1 -1
- package/dist/core/command-generation/generator.js +0 -17
- package/dist/core/command-generation/generator.js.map +1 -1
- package/dist/core/command-generation/index.d.ts +2 -5
- package/dist/core/command-generation/index.d.ts.map +1 -1
- package/dist/core/command-generation/index.js +0 -9
- package/dist/core/command-generation/index.js.map +1 -1
- package/dist/core/command-generation/types.d.ts +10 -36
- package/dist/core/command-generation/types.d.ts.map +1 -1
- package/dist/core/command-generation/types.js +0 -6
- package/dist/core/command-generation/types.js.map +1 -1
- package/dist/core/command-generation/yaml.d.ts +3 -18
- package/dist/core/command-generation/yaml.d.ts.map +1 -1
- package/dist/core/command-generation/yaml.js +5 -23
- package/dist/core/command-generation/yaml.js.map +1 -1
- package/dist/core/config-prompts.d.ts +2 -4
- package/dist/core/config-prompts.d.ts.map +1 -1
- package/dist/core/config-prompts.js +2 -7
- package/dist/core/config-prompts.js.map +1 -1
- package/dist/core/config-schema.d.ts +8 -53
- package/dist/core/config-schema.d.ts.map +1 -1
- package/dist/core/config-schema.js +49 -62
- package/dist/core/config-schema.js.map +1 -1
- package/dist/core/config.d.ts +39 -26
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +36 -22
- package/dist/core/config.js.map +1 -1
- package/dist/core/dashboard-activity.d.ts +7 -9
- package/dist/core/dashboard-activity.d.ts.map +1 -1
- package/dist/core/dashboard-activity.js +26 -24
- package/dist/core/dashboard-activity.js.map +1 -1
- package/dist/core/dashboard-data.d.ts +40 -22
- package/dist/core/dashboard-data.d.ts.map +1 -1
- package/dist/core/dashboard-data.js +62 -70
- package/dist/core/dashboard-data.js.map +1 -1
- package/dist/core/global-config.d.ts +24 -53
- package/dist/core/global-config.d.ts.map +1 -1
- package/dist/core/global-config.js +38 -67
- package/dist/core/global-config.js.map +1 -1
- package/dist/core/init.d.ts +25 -19
- package/dist/core/init.d.ts.map +1 -1
- package/dist/core/init.js +168 -181
- package/dist/core/init.js.map +1 -1
- package/dist/core/list.d.ts +1 -1
- package/dist/core/list.d.ts.map +1 -1
- package/dist/core/list.js +121 -28
- package/dist/core/list.js.map +1 -1
- package/dist/core/local-server.d.ts +41 -83
- package/dist/core/local-server.d.ts.map +1 -1
- package/dist/core/local-server.js +53 -98
- package/dist/core/local-server.js.map +1 -1
- package/dist/core/markdown-render.d.ts +25 -0
- package/dist/core/markdown-render.d.ts.map +1 -0
- package/dist/core/markdown-render.js +94 -0
- package/dist/core/markdown-render.js.map +1 -0
- package/dist/core/migrate.d.ts +29 -14
- package/dist/core/migrate.d.ts.map +1 -1
- package/dist/core/migrate.js +184 -122
- package/dist/core/migrate.js.map +1 -1
- package/dist/core/parsers/change-parser.d.ts +7 -10
- package/dist/core/parsers/change-parser.d.ts.map +1 -1
- package/dist/core/parsers/change-parser.js +48 -56
- package/dist/core/parsers/change-parser.js.map +1 -1
- package/dist/core/parsers/markdown-parser.d.ts +8 -9
- package/dist/core/parsers/markdown-parser.d.ts.map +1 -1
- package/dist/core/parsers/markdown-parser.js +31 -22
- package/dist/core/parsers/markdown-parser.js.map +1 -1
- package/dist/core/parsers/requirement-blocks.d.ts +43 -15
- package/dist/core/parsers/requirement-blocks.d.ts.map +1 -1
- package/dist/core/parsers/requirement-blocks.js +142 -70
- package/dist/core/parsers/requirement-blocks.js.map +1 -1
- package/dist/core/parsers/requirement-text.d.ts +73 -79
- package/dist/core/parsers/requirement-text.d.ts.map +1 -1
- package/dist/core/parsers/requirement-text.js +137 -79
- package/dist/core/parsers/requirement-text.js.map +1 -1
- package/dist/core/parsers/spec-structure.d.ts.map +1 -1
- package/dist/core/parsers/spec-structure.js +10 -6
- package/dist/core/parsers/spec-structure.js.map +1 -1
- package/dist/core/profiles.d.ts +3 -10
- package/dist/core/profiles.d.ts.map +1 -1
- package/dist/core/profiles.js +5 -12
- package/dist/core/profiles.js.map +1 -1
- package/dist/core/project-config.d.ts +43 -44
- package/dist/core/project-config.d.ts.map +1 -1
- package/dist/core/project-config.js +107 -82
- package/dist/core/project-config.js.map +1 -1
- package/dist/core/project-layout.d.ts +9 -17
- package/dist/core/project-layout.d.ts.map +1 -1
- package/dist/core/project-layout.js +16 -26
- package/dist/core/project-layout.js.map +1 -1
- package/dist/core/root-selection.d.ts +11 -7
- package/dist/core/root-selection.d.ts.map +1 -1
- package/dist/core/root-selection.js +7 -8
- package/dist/core/root-selection.js.map +1 -1
- package/dist/core/rules.d.ts +6 -1
- package/dist/core/rules.d.ts.map +1 -1
- package/dist/core/rules.js +23 -2
- package/dist/core/rules.js.map +1 -1
- package/dist/core/schema-names.d.ts +16 -0
- package/dist/core/schema-names.d.ts.map +1 -0
- package/dist/core/schema-names.js +16 -0
- package/dist/core/schema-names.js.map +1 -0
- package/dist/core/schemas/base.schema.d.ts +3 -0
- package/dist/core/schemas/base.schema.d.ts.map +1 -1
- package/dist/core/schemas/base.schema.js +22 -6
- package/dist/core/schemas/base.schema.js.map +1 -1
- package/dist/core/schemas/change.schema.d.ts +16 -0
- package/dist/core/schemas/change.schema.d.ts.map +1 -1
- package/dist/core/schemas/change.schema.js +41 -10
- package/dist/core/schemas/change.schema.js.map +1 -1
- package/dist/core/schemas/spec.schema.d.ts +2 -0
- package/dist/core/schemas/spec.schema.d.ts.map +1 -1
- package/dist/core/shared/index.d.ts +3 -8
- package/dist/core/shared/index.d.ts.map +1 -1
- package/dist/core/shared/index.js +3 -8
- package/dist/core/shared/index.js.map +1 -1
- package/dist/core/shared/rules-generation.d.ts +19 -8
- package/dist/core/shared/rules-generation.d.ts.map +1 -1
- package/dist/core/shared/rules-generation.js +117 -50
- package/dist/core/shared/rules-generation.js.map +1 -1
- package/dist/core/shared/skill-generation.d.ts +28 -43
- package/dist/core/shared/skill-generation.d.ts.map +1 -1
- package/dist/core/shared/skill-generation.js +82 -51
- package/dist/core/shared/skill-generation.js.map +1 -1
- package/dist/core/shared/tool-detection.d.ts +39 -61
- package/dist/core/shared/tool-detection.d.ts.map +1 -1
- package/dist/core/shared/tool-detection.js +88 -80
- package/dist/core/shared/tool-detection.js.map +1 -1
- package/dist/core/skill-metrics.d.ts +36 -63
- package/dist/core/skill-metrics.d.ts.map +1 -1
- package/dist/core/skill-metrics.js +34 -73
- package/dist/core/skill-metrics.js.map +1 -1
- package/dist/core/spec-presenter.js +6 -6
- package/dist/core/spec-presenter.js.map +1 -1
- package/dist/core/specs-apply.d.ts +22 -23
- package/dist/core/specs-apply.d.ts.map +1 -1
- package/dist/core/specs-apply.js +166 -193
- package/dist/core/specs-apply.js.map +1 -1
- package/dist/core/templates/fragments/interview.d.ts +2 -6
- package/dist/core/templates/fragments/interview.d.ts.map +1 -1
- package/dist/core/templates/fragments/interview.js +2 -6
- package/dist/core/templates/fragments/interview.js.map +1 -1
- package/dist/core/templates/fragments/next-step.d.ts +4 -8
- package/dist/core/templates/fragments/next-step.d.ts.map +1 -1
- package/dist/core/templates/fragments/next-step.js +4 -8
- package/dist/core/templates/fragments/next-step.js.map +1 -1
- package/dist/core/templates/fragments/validate.d.ts +13 -0
- package/dist/core/templates/fragments/validate.d.ts.map +1 -0
- package/dist/core/templates/fragments/validate.js +13 -0
- package/dist/core/templates/fragments/validate.js.map +1 -0
- package/dist/core/templates/fragments/verify.d.ts +9 -12
- package/dist/core/templates/fragments/verify.d.ts.map +1 -1
- package/dist/core/templates/fragments/verify.js +9 -12
- package/dist/core/templates/fragments/verify.js.map +1 -1
- package/dist/core/templates/index.d.ts +0 -6
- package/dist/core/templates/index.d.ts.map +1 -1
- package/dist/core/templates/index.js +0 -7
- package/dist/core/templates/index.js.map +1 -1
- package/dist/core/templates/skill-templates.d.ts +1 -5
- package/dist/core/templates/skill-templates.d.ts.map +1 -1
- package/dist/core/templates/skill-templates.js +0 -5
- package/dist/core/templates/skill-templates.js.map +1 -1
- package/dist/core/templates/types.d.ts +3 -7
- package/dist/core/templates/types.d.ts.map +1 -1
- package/dist/core/templates/types.js +0 -3
- package/dist/core/templates/types.js.map +1 -1
- package/dist/core/templates/workflows/apply.d.ts +3 -9
- package/dist/core/templates/workflows/apply.d.ts.map +1 -1
- package/dist/core/templates/workflows/apply.js +11 -13
- package/dist/core/templates/workflows/apply.js.map +1 -1
- package/dist/core/templates/workflows/archive.d.ts +0 -6
- package/dist/core/templates/workflows/archive.d.ts.map +1 -1
- package/dist/core/templates/workflows/archive.js +19 -6
- package/dist/core/templates/workflows/archive.js.map +1 -1
- package/dist/core/templates/workflows/decision.js +4 -4
- package/dist/core/templates/workflows/decision.js.map +1 -1
- package/dist/core/templates/workflows/explore.js +1 -1
- package/dist/core/templates/workflows/grill.d.ts.map +1 -1
- package/dist/core/templates/workflows/grill.js +0 -2
- package/dist/core/templates/workflows/grill.js.map +1 -1
- package/dist/core/templates/workflows/issue.d.ts +0 -6
- package/dist/core/templates/workflows/issue.d.ts.map +1 -1
- package/dist/core/templates/workflows/issue.js +3 -2
- package/dist/core/templates/workflows/issue.js.map +1 -1
- package/dist/core/templates/workflows/propose.d.ts +0 -6
- package/dist/core/templates/workflows/propose.d.ts.map +1 -1
- package/dist/core/templates/workflows/propose.js +2 -2
- package/dist/core/templates/workflows/propose.js.map +1 -1
- package/dist/core/templates/workflows/sync.d.ts +2 -8
- package/dist/core/templates/workflows/sync.d.ts.map +1 -1
- package/dist/core/templates/workflows/sync.js +4 -3
- package/dist/core/templates/workflows/sync.js.map +1 -1
- package/dist/core/templates/workflows/update.d.ts +0 -6
- package/dist/core/templates/workflows/update.d.ts.map +1 -1
- package/dist/core/templates/workflows/update.js +9 -2
- package/dist/core/templates/workflows/update.js.map +1 -1
- package/dist/core/update.d.ts +23 -23
- package/dist/core/update.d.ts.map +1 -1
- package/dist/core/update.js +139 -118
- package/dist/core/update.js.map +1 -1
- package/dist/core/user-state-migration.d.ts +13 -15
- package/dist/core/user-state-migration.d.ts.map +1 -1
- package/dist/core/user-state-migration.js +16 -20
- package/dist/core/user-state-migration.js.map +1 -1
- package/dist/core/validation/constants.d.ts +4 -10
- package/dist/core/validation/constants.d.ts.map +1 -1
- package/dist/core/validation/constants.js +25 -20
- package/dist/core/validation/constants.js.map +1 -1
- package/dist/core/validation/prose-length.d.ts +15 -0
- package/dist/core/validation/prose-length.d.ts.map +1 -0
- package/dist/core/validation/prose-length.js +29 -0
- package/dist/core/validation/prose-length.js.map +1 -0
- package/dist/core/validation/purpose-placeholder.d.ts +9 -16
- package/dist/core/validation/purpose-placeholder.d.ts.map +1 -1
- package/dist/core/validation/purpose-placeholder.js +30 -44
- package/dist/core/validation/purpose-placeholder.js.map +1 -1
- package/dist/core/validation/section-validator.d.ts +4 -4
- package/dist/core/validation/section-validator.d.ts.map +1 -1
- package/dist/core/validation/section-validator.js +43 -7
- package/dist/core/validation/section-validator.js.map +1 -1
- package/dist/core/validation/task-numbering.d.ts +6 -3
- package/dist/core/validation/task-numbering.d.ts.map +1 -1
- package/dist/core/validation/task-numbering.js +23 -11
- package/dist/core/validation/task-numbering.js.map +1 -1
- package/dist/core/validation/types.d.ts +18 -0
- package/dist/core/validation/types.d.ts.map +1 -1
- package/dist/core/validation/types.js +12 -1
- package/dist/core/validation/types.js.map +1 -1
- package/dist/core/validation/validator.d.ts +43 -63
- package/dist/core/validation/validator.d.ts.map +1 -1
- package/dist/core/validation/validator.js +500 -267
- package/dist/core/validation/validator.js.map +1 -1
- package/dist/prompts/searchable-multi-select.d.ts +3 -8
- package/dist/prompts/searchable-multi-select.d.ts.map +1 -1
- package/dist/prompts/searchable-multi-select.js +16 -39
- package/dist/prompts/searchable-multi-select.js.map +1 -1
- package/dist/utils/change-metadata.d.ts +11 -50
- package/dist/utils/change-metadata.d.ts.map +1 -1
- package/dist/utils/change-metadata.js +48 -67
- package/dist/utils/change-metadata.js.map +1 -1
- package/dist/utils/change-utils.d.ts +30 -54
- package/dist/utils/change-utils.d.ts.map +1 -1
- package/dist/utils/change-utils.js +133 -91
- package/dist/utils/change-utils.js.map +1 -1
- package/dist/utils/file-lock.d.ts +39 -0
- package/dist/utils/file-lock.d.ts.map +1 -0
- package/dist/utils/file-lock.js +149 -0
- package/dist/utils/file-lock.js.map +1 -0
- package/dist/utils/file-system.d.ts +12 -32
- package/dist/utils/file-system.d.ts.map +1 -1
- package/dist/utils/file-system.js +16 -40
- package/dist/utils/file-system.js.map +1 -1
- package/dist/utils/frontmatter.d.ts +7 -11
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +11 -11
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/interactive.d.ts +4 -9
- package/dist/utils/interactive.d.ts.map +1 -1
- package/dist/utils/interactive.js +2 -4
- package/dist/utils/interactive.js.map +1 -1
- package/dist/utils/item-discovery.d.ts +15 -10
- package/dist/utils/item-discovery.d.ts.map +1 -1
- package/dist/utils/item-discovery.js +42 -47
- package/dist/utils/item-discovery.js.map +1 -1
- package/dist/utils/link.d.ts +9 -18
- package/dist/utils/link.d.ts.map +1 -1
- package/dist/utils/link.js +9 -18
- package/dist/utils/link.js.map +1 -1
- package/dist/utils/requirement-diff.d.ts +13 -23
- package/dist/utils/requirement-diff.d.ts.map +1 -1
- package/dist/utils/requirement-diff.js +13 -23
- package/dist/utils/requirement-diff.js.map +1 -1
- package/dist/utils/spec-files.d.ts +7 -20
- package/dist/utils/spec-files.d.ts.map +1 -1
- package/dist/utils/spec-files.js +26 -51
- package/dist/utils/spec-files.js.map +1 -1
- package/dist/utils/task-progress.d.ts +11 -9
- package/dist/utils/task-progress.d.ts.map +1 -1
- package/dist/utils/task-progress.js +53 -32
- package/dist/utils/task-progress.js.map +1 -1
- package/dist/utils/timestamp.d.ts +5 -8
- package/dist/utils/timestamp.d.ts.map +1 -1
- package/dist/utils/timestamp.js +5 -8
- package/dist/utils/timestamp.js.map +1 -1
- package/package.json +2 -3
- package/schemas/decision/templates/decision.md +3 -1
- package/schemas/decision/templates/index.md +2 -2
- package/schemas/issue/schema.yaml +8 -1
- package/schemas/issue/templates/spec.md +27 -2
- package/schemas/sdd/schema.yaml +20 -1
- package/schemas/sdd/templates/spec.md +27 -2
- /package/assets/rules/tospec/{single-sourc-of-truth.md → single-source-of-truth.md} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,247 +6,83 @@
|
|
|
6
6
|
|
|
7
7
|
tospec 是一套 spec-driven development CLI: 以 schema 定義文件結構與工作流程, 進度由檔案系統狀態推算, 開發方法則封裝於 Skill 之中, 使 AI 工具 (Claude Code / Codex) 得以循序完成需求釐清、規格撰寫、設計、任務拆解、實作到歸檔的完整流程.
|
|
8
8
|
|
|
9
|
-
## [0.
|
|
9
|
+
## [0.20.0] - 2026-09-23
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
本版本整合 0.19.0-beta.0 至 beta.14 共十五個預發布版本, 是 0.18.0 之後的第一個正式版. 起點是跟進上游 OpenSpec 1.12.0, 其後是對整個指令面連續多輪的掃描與審查. 修掉的缺陷絕大多數共用同一種形狀: **指令回報的結論比它實際檢查過的內容更強** — 兩個本該一致的答案已經分岔而沒有人察覺, 或檢查確實存在卻到不了它為之而寫的那個案例. 因此本版的主軸不是新功能, 而是讓 `validate`、`archive`、`status`、`instructions` 與 skill 對同一個 change 說同一句話, 並讓 `--json` 呼叫端 (也就是 agent) 拿到人類模式早已看得到的每一項診斷.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
### 破壞性變更
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
- **`
|
|
18
|
-
|
|
19
|
-
- **
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
- **`tospec
|
|
26
|
-
|
|
27
|
-
- **`tospec-issue` 在寫入 task.md 時必須對帳 Test Plan 與 Tasks**: skill 的第 6 步與 guardrail 新增一條規則 — Test Plan 的每一條待補測試, 都必須對應到一個編號 task, 或在 Test Plan 裡說明為何不需要. 這是本版第二層複查發現的流失途徑: `tospec validate` 看到的是兩段散文, apply 則在 Tasks 的勾選框打完時回報完成, 所以一條沒有變成 task 的 Test Plan 項目會靜默消失, 而該 change 看起來仍然是完整的. 兩個實例都由此而來 — dashboard 的 `--host 0.0.0.0 --allow-remote --detach` 回歸測試與 decision 的同秒不同 topic 測試. 另評估過放在 `tospec-apply` 的 `VERIFY.md`: 那裡是複查時才觸發, 且依其定義是選用的 (「run when the user asks for a review」), 擋不下這一批.
|
|
28
|
-
|
|
29
|
-
- **`tospec init` 不再指向不存在的 `README.md`**: 收尾訊息中無條件輸出的 `Documentation: README.md` 已移除. 那行大概是為套件自身的 README 而寫, 但它在使用者的新專案裡呈現為一個專案相對路徑, 而 `init` 只寫入 `tospec/`、`.agents/` 與設定的工具目錄, 不會產生該檔. 相較於改指向套件首頁 URL, 直接移除較安全: `init` 本來就以具體的下一步作結, 而一個必須持續保持正確的連結, 就是一個還會再次過期的連結.
|
|
30
|
-
|
|
31
|
-
### 修正
|
|
32
|
-
|
|
33
|
-
- **`tospec status` 從不讀取 `skip_specs`, 宣告無規格的 change 永遠停在未完成**: `skip_specs` 原本只有兩個消費者 — validate 的 `skipSpecsMarkerIssues` 與 archive 的 `options.skipSpecs` 分支 — `src/core/artifact-graph/` 底下一次都沒出現. 根因是 artifact graph 純粹由檔案存在性與 schema 的 `optional` 旗標推算狀態, 而 `skip_specs` 正是針對 schema 層級預設值的**逐 change 例外**, graph 沒有任何途徑看見它; 三個命令因此對同一個 change 給出不同答案. 修正讓 status 讀取 validate 與 archive 早已在讀的同一個標記 (`readSkipSpecsMarker`), 並將該 artifact 回報為 `skipped` 而非 `ready`, 同時把格式錯誤的值以 `invalidReason` 揭露而不是靜默略過. 另一個選項是在 sdd schema 裡把 `specs` 標成 `optional: true`, 但那會對每一個 sdd change 取消這項要求, 與 schema 的本意正好相反.
|
|
34
|
-
|
|
35
|
-
- **`tospec dashboard --detach` 為綁定失敗的 child 印出 URL 並以狀態碼 0 結束**: `child.pid === undefined` 只攔得住 `spawn` 本身失敗; 成功 spawn 之後所有可能出錯的事 — `--allow-remote` 拒絕、`EADDRINUSE`、權限錯誤、啟動時的例外 — 全發生在 `stdio: 'ignore'` 之後而無從觀測, parent 接著還為一個正在結束的 process 寫下 pid 紀錄. 修正是讓 child 有辦法回報結果: stderr 改為 `pipe`, child 綁定成功後送出 `LISTENING <url>`, parent 等到這一行才寫 pid 紀錄並印出 URL, 失敗則轉述 child 自己的錯誤文字並以非零狀態碼結束. 在 parent 預先檢查 host 只能修好重現步驟裡的那一種, 對 `EADDRINUSE` 或任何未來的啟動失敗仍會回報成功.
|
|
36
|
-
- **等待本身加上逾時**: 原先的握手只 race `LISTENING` 與 `exit` 兩個事件, 但一個被 spawn 的 process 有三種結局 — 成功、失敗, 以及**兩者皆非**. child 若綁定後卡住, parent 會永遠等下去, `--detach` 直接 hang 且沒有任何輸出. 現加上 10 秒逾時, 訊息說明的是「沒有觀測到 child 回報」而非宣稱啟動失敗 (child 仍在執行, 可能只是還沒起來), 並指向 `--list` / `--stop`. `child.unref()` 一併移進 `finally`: 在逾時這條路徑上 child 還活著, 未 unref 的 handle 會在錯誤都回報完之後繼續綁住 parent 的 event loop.
|
|
37
|
-
- **成功路徑補上真實 spawn 的測試**: 原本唯一的覆蓋是自己 emit `LISTENING http://...` 的 stub, 與實作對同一個字串雙向耦合 — 這種閉環只有在兩邊同時寫錯時才會失敗. 新的測試實際起一個 detached dashboard, 用 `--list` 確認看得到, 再以 `--stop` 收尾, 全程不提及那個 token.
|
|
38
|
-
|
|
39
|
-
- **`tospec dashboard --port` 完全沒有驗證**: `Number(options?.port ?? 5620)` 沒有 `Number.isInteger` 或範圍檢查, 所以 `notanumber` 變成 `NaN` 一路抵達 URL 字串; 連埠號遞增迴圈也擋不住, 因為 `used.has(NaN)` 恆為 false, 該值原封不動通過. 現以明確的整數與 `0..65535` 範圍檢查走標準錯誤路徑, `-p 0` 維持可用 — 它是把埠號選擇交給作業系統, `--list` 本來就正確回報實際埠號.
|
|
40
|
-
|
|
41
|
-
- **`tospec decision new --force` 覆寫檔案卻仍追加一列索引**: `appendIndexRow` 是無條件的, 當 `--force` 取代既有檔案時該檔的列已經存在, 於是索引多出一列重複. 既有的 `ponytail` 註解記錄了「append-only、不去重」這個決定, 但它設想的是「對同一個 topic 重跑 `new` 會多一列」, 沒有涵蓋 `--force` — 那裡的檔案並不是第二筆紀錄. 修正讓索引列以「這次寫入是新檔」為條件, 符合索引本身的語意: 一筆決策一列, 而不是一次呼叫一列.
|
|
42
|
-
- **存在性檢查不是原子的**: 檢查與寫入之間隔著 `loadTemplate` 與 `renderDecision`, 那是貨真價實的 I/O 寬度; 兩個 process 可以同時通過檢查、同時寫入, 而 `fs.writeFileSync` 預設的 `'w'` 旗標無條件截斷, 後者靜默覆蓋前者. 現改用 `{ flag: 'wx' }` 讓檔案系統來執行這道守衛, `EEXIST` 翻譯回既有的訊息, 使用者可見的行為不變; `--force` 維持 `'w'`. 任何 check-then-write 的方案都留有一個窗口, 無論把它縮到多小, 而檔案系統早就提供了這個原語.
|
|
43
|
-
|
|
44
|
-
- **capability 資料夾內檔名寫錯的 delta 驗證全綠卻被靜默丟棄**: `findSpecFiles` 只收集 basename 恰為 `spec.md` 的檔案, 而 validator 早已備有一整組針對「永遠不會被合併的 delta 檔」的守衛 (specs 根目錄的 `spec.md`、深度超過一層的路徑、點開頭的資料夾), 每一條都以同一句理由成立. 第四種失敗模式完全相同的情況 — 對的資料夾、錯的檔名 — 沒有守衛, 根因是**它在守衛迴圈看到之前就被 walker 過濾掉了**: 守衛只能裁決 walker 交給它的檔案. 合併端讀的同樣是寫死的 `spec.md`, 兩邊一致, 所以沒有任何地方回報衝突. 修正是新增一個回傳 `specs/` 下所有 `*.md` 的走訪器並在同一個迴圈裡以 ERROR 指名該檔, 而不是放寬合併路徑 — 合併只讀 `spec.md` 是版面配置的契約, 改動它會讓同一資料夾內的兩個檔案變得語意不明.
|
|
45
|
-
|
|
46
|
-
- **`tospec config get` 對未知的 key 與未設定的 key 都靜默失敗**: `getNestedValue` 對「這個 key 存在但沒有值」與「這個 key 不屬於 schema」都回傳 `undefined`, 單一的 `undefined` 分支於是把兩種不同的使用者錯誤壓成同一次無聲離開. 根因是 `get` 從不驗證 key 是否為真 — `set` 不會有這個問題, 因為它在寫入前會對照 schema 驗證. 現先驗證 key (沿用 `set` 的判準, 兩個命令對「什麼是有效的 key」保持一致), 未知的 key 與未設定的 key 各給一則 stderr 診斷. stdout 在兩種情況下都維持空白, 保住 `(raw, scriptable)` 的契約.
|
|
47
|
-
|
|
48
|
-
- **`tospec migrate` 丟棄 `openspec/project.md` 並寫出一行式的 `config.yaml`**: `migrate.ts` 裡搜不到 `project.md` 一字. 這比表面上嚴重: `openspec/project.md` 就是 OpenSpec 的專案脈絡, 其 tospec 對應物是 `tospec/config.yaml` 的 `context:` — `tospec instructions` 餵給 agent 的那個值 — 靜默丟失它等於把專案的技術棧、慣例與領域知識從其後每一份 artifact 的 prompt 裡拿掉. 由於 OpenSpec 專案的脈絡放在 `project.md` 而不是 `config.yaml`, 「來源沒有 config.yaml」才是真實遷移的常見路徑, 而那條分支只寫一行, 使用者連放回去的位置都看不到. 現將其內容折進 `context:` 區塊 (而非另存為 `tospec/project.md` — `context:` 才是 CLI 真正會讀的欄位, 放在 `tospec/` 下的 `project.md` 不會被任何東西讀取), 絕對分支改用 `init` 所用的同一份模板, 並在 summary 裡據實說明脈絡被帶過去、因既有 context 而未帶、來源為空, 或根本不存在.
|
|
49
|
-
- **產生的 YAML block scalar 補上明確縮排指示字元**: `context: |` 沒有 indentation indicator, 而 YAML literal block 的縮排是由**第一個非空行**推斷的, 所以 `project.md` 若以縮排行開頭 (四空格 code block、縮排清單), 推斷值會變成 6, 其後每一行正常縮排都不足而使區塊中途終止, 產出一份無法 parse 的 `config.yaml` — 而 migration 仍印出「Context: project.md -> config.yaml context:」並以狀態碼 0 結束, 與這個 change 本來要修的靜默失敗同類. 現改為 `context: |2`, 讓縮排不再取決於被序列化的內容.
|
|
50
|
-
- **遷移的 change 不補 ticket stub**: summary 會回報有多少個 change 需要補. ticket 檔名帶時間戳, 而其語意是「這項工作被提出的時間」, migrate 推導不出正確的值 — 遷移當下的時間、檔案 mtime、OpenSpec 封存目錄名只有日期的前綴, 全都是編造. 產生一份時間不可信的 ticket, 會把一個本來只是「缺少」的狀態變成「存在但內容錯誤」, 而後者更難發現. 另補上「遷移結果通過 `tospec validate --all`」的回歸測試, 把「缺 ticket 不影響驗證」這個前提釘住. 決策記錄: `tospec/decisions/20260916_121552-migrated-changes-defer-ticket-stubs.md`.
|
|
51
|
-
|
|
52
|
-
- **數個 `--json` 失敗缺少 null 資料鍵, 或根本沒有輸出 JSON 文件**: `show` 的非互動提示只寫 `console.error` 而從不檢查 `options.json`, 同檔案裡另外兩個失敗分支 (`unknown_item`、`ambiguous_item`) 都正確地經由 `emitFailureStatus` 帶上 `{ item: null, root }`, 唯獨這一個被漏掉; `instructions` 的失敗則完全沒有傳 payload. 根因不是這兩處各自寫錯, 而是**沒有任何機制要求一個新命令必須具備失敗 payload**, 所以漂移是預設值. 除了補齊兩處, `test/cli/json-failure.test.ts` 由逐命令案例改寫為表格驅動的全面掃描, 少了失敗 payload 的新命令現在預設就會讓測試失敗. `show --type` 一併改用 commander 的 `.choices(['change', 'spec'])`, 讓「無效的值」不再與「沒有給值」一樣被折成 `undefined` — 檢查因此與選項宣告放在一起, 和 `--sort` 既有的做法一致.
|
|
53
|
-
|
|
54
|
-
- **`tospec decision new` 的 topic 錯誤訊息自稱「Change name」**: 重用 `validateChangeName` 來檢查文法是對的 — topic 與 change name 共用同一套 kebab-id 文法, 而 `change-utils.ts` 是該文法唯一的定義處 — 缺陷在於它的錯誤字串是為單一呼叫端撰寫的, 另一個呼叫端直接包裝而未翻譯. 現讓 `validateChangeName` 接受呼叫端提供的名詞 (預設 `'Change name'`), `decision new` 傳入 `'Topic'`. 在 `decision` 呼叫點改寫字串會留下兩份會各自漂移的訊息, 修在共用驗證器才能維持一套文法、一套訊息.
|
|
55
|
-
|
|
56
|
-
- **`tospec validate` 的 Next steps 對放置錯誤印出無關的 delta 建議**: 判定一則 issue 是否與 delta 有關的依據是「路徑以 `spec.md` 結尾」, 而同一批改動新增的錯檔名守衛會產出 `auth/extra.md`、`auth/README.md` 這類路徑, 使該註解宣稱的前提不再成立. 更直接的是它反向也錯: specs 根目錄的放置錯誤路徑恰為 `spec.md`, 因此會收到三條通用的 delta 建議 — 而 `printNextSteps` 存在的意義正是不要「為另一種失敗給建議」. 判定改為 `endsWith('/spec.md')`: 差一個斜線, 但它讓三種鄰近形狀都落在正確的一側 — 兩種放置錯誤 (訊息本身已指名該改成什麼路徑) 與整個 change 的 `file` 哨兵 (其訊息經 `enrichTopLevelError` 後已含完全相同的三條建議).
|
|
57
|
-
|
|
58
|
-
### 其他
|
|
59
|
-
|
|
60
|
-
- **`tospec dashboard --detach` 的兩個 pid record writer 刻意保留, 並補上等價性測試**: parent 與 detached child 都會寫同一份 pid record. 複查一度認定 parent 那次是純重複, 實際判定是兩者各自為某一種失敗模式下唯一存在的紀錄 — child 那次是每個 dashboard (含前景執行) 為自己寫的, 也是 parent 在握手中途被砍或逾時放棄時僅存的一份; parent 那次則保證呼叫回傳時紀錄已經存在, 因為 child 是**送出 `LISTENING` 之後**才寫自己的, 少了它, 緊接著的第二次 `--detach` 會找不到東西可擋, 而綁定後卡住的 child 會變成追蹤不到的 orphan. 兩者寫入的位元組相同, docstring 已據實說明順序與理由; 新測試從子目錄啟動, 讓兩邊各自推導 `projectRoot` 再斷言等價, 一旦哪天分岔就會失敗.
|
|
61
|
-
|
|
62
|
-
- **本版第二層複查的基準與結果**: 十個 change 的 task.md 逐條對照實際程式碼, 加上 `pnpm build && pnpm test` 全綠 — 九項發現全部是「測試通過但仍然錯」的情況. 其中兩項有可獨立執行的紅燈訊號 (YAML block scalar 的 parse 錯誤、REMOVED delta 的假 ERROR), 其餘是註解與實際行為的落差, 或是邊界情況本來就沒有斷言. 複查修正完成後為 83 個檔案 / 1112 passed, 較基準新增 17 個測試; 1 skipped 為既有的 `it.runIf(platform !== 'win32')`, 與本版改動無關.
|
|
63
|
-
|
|
64
|
-
- 上游 OpenSpec 參考點仍停在 `e062b95` (1.12.0), 本版與上游比對無關.
|
|
65
|
-
|
|
66
|
-
## [0.19.0-beta.6] - 2026-09-14
|
|
67
|
-
|
|
68
|
-
**預發布版本**: 需以 `npm install @seanmars/tospec@beta` 明確指定才會安裝, `latest` 仍指向 0.18.0.
|
|
69
|
-
|
|
70
|
-
本版本來自對 `feature/sync-1.12.0` 整條分支的逐行審查 (165 個檔案, 約 10.9k 行新增), 共回報 14 項發現: 12 項修正, 2 項查證後判定不需修改. 這些缺陷散落在七個模組, 卻只有兩種形狀. 其一是**條件寫成「當初想得到的那幾種寫法」, 而不是它所代表的概念** — wildcard 位址只列三種拼法、空字串被當成使用者提供的名稱、殘留偵測掃了比目標更大的目錄; 每一條單獨讀都成立, 遇到沒被列舉的那一個就靜默走錯分支. 其二是**重構只搬走主要路徑, 邊界上的既有保證留在原地** — 清理被關進「這次有寫入東西」的條件裡、讀取失敗從「照舊附加」變成「整段放棄」、診斷訊息跟著 verdict 分流時漏掉通過的那一半. 本版另推翻 0.19.0-beta.5 引入的 skill 連結交付: 該機制在單一平台上是對的, 在跨平台團隊裡卻使提交進 git 的內容取決於貢獻者的作業系統.
|
|
71
|
-
|
|
72
|
-
### 變更
|
|
73
|
-
|
|
74
|
-
- **相依工具的 skill 目錄改以複製交付, 不再使用連結 (breaking change)**: `tospec init` / `tospec update` 寫入 `.claude/skills/tospec-*` 與 `.codex/skills/tospec-*` 的方式, 由指向 `.agents/skills/` 的連結改回獨立複製. 症狀是提交進 git 的內容會隨貢獻者的作業系統改變 — 連結沒有跨平台的統一寫法: Windows 用 junction, git 會穿透它並記錄成一般的 `100644` 檔案; POSIX 用 symlink, git 記錄成 mode `120000` 且 blob 內容就是目標路徑字串. 於是 POSIX 貢獻者跑一次 `update` 會把二十個一般檔案變成 `120000` blob, Windows 貢獻者再跑一次又變回來, 雙方都不是有意修改內容, 卻都在審查裡看起來像刻意的編輯. 更嚴重的是本專案 `core.symlinks` 為 `false`: 這種 blob 取出後會變成一個內容為 `../../.agents/skills/tospec-apply` 的文字檔佔住 skill 目錄的位置, 所有 agent 靜默載入不到任何東西. 根因在交付機制本身依賴平台, 不在 git 設定, 所以先嘗試的 gitignore 方案被否決 — 它只是把依賴作業系統的機制藏起來, 還在「clone 完成」與「手上有 skill」之間插入一個建置步驟, 代價正好落在這次要照顧的跨平台隊友身上. 換得的代價是編輯 `.agents/skills/` 後需重跑 `tospec update` 才會反映到工具目錄, 而本專案的 skill 正文本來就由 TypeScript 產生, 重跑 update 早已是既定流程. 決策記錄: `tospec/decisions/20260914_205300-copy-dependent-skill-dirs-for-cross-platform-teams.md`.
|
|
75
|
-
- **舊版留下的連結不需要遷移步驟**: `copyDir` 會先清空目的地, 而 `fs.rm(recursive)` 對 junction 或 symlink 是解除連結而非遞迴刪除, 因此停在 0.19.0-beta.5 的專案下次執行 `tospec update` 即就地轉換為複製, canonical 目錄不受影響.
|
|
76
|
-
- `init` 的輸出不再區分 linked / copied 兩種結果, 改為單一行說明哪些目標拿到複製, 以及編輯 canonical skill 後需要重跑; `DeliveryKind` 匯出一併移除.
|
|
77
|
-
- 這推翻的是 `20260910_214500` 的交付機制一項, 該 ADR 其餘決定 (agents 列為一般可選取列、`requires` 取代 always-on、清空選單等同 `--tools none`、偵測不到時的回退) 全數不受影響; 舊 ADR 已標記為部分被取代.
|
|
78
|
-
|
|
79
|
-
- **`tospec archive` 不再重跑一次合併預檢**: 驗證流程裡的 `findArchiveBlockers` 會逐一重建每份受影響的 spec — 每個 capability 都重讀並重新解析 delta 與整份主 spec — 藉此以 INFO 等級回報合併會拒絕什麼. 對 `tospec validate` 而言這正是它的用途: 作者在嘗試歸檔前就知道. 對 archive 而言則是同一份工作做兩次, 因為 archive 緊接著就執行真正的合併, 而真正的合併早已把同一個前置條件轉成 `archive_spec_update_failed` 並以非零狀態碼結束 — 那是 INFO 等級本來就辦不到的事. 改為新增 `archivePreflight` 選項, archive 傳入 `false`. 「validate 綠燈 ⟺ archive 通過驗證」這項等價關係談的是 verdict, 而 INFO 從來不影響 verdict.
|
|
80
|
-
|
|
81
|
-
### 修正
|
|
82
|
-
|
|
83
|
-
- **`--allow-remote` 搭配 `::0` 等寫法會綁定所有介面後拒絕每一個請求**: `WILDCARD_HOSTS` 收的是字串拼法而非位址, 只列了 `0.0.0.0`、`::` 與完整展開的 IPv6 形式. `tospec dashboard --allow-remote --host ::0` 因此通過綁定守衛、在每個介面上監聽, 然後 `acceptsAnyHostHeader` 回報 false, 使每一個從其他機器抵達的請求都因 Host header 既非 loopback 也不字面等於 `::0` 而被 403. 這正是 `20260910_150312` 當初要消除的「綁定後拒絕全部」失效, 只是換一種同義的位址拼法就再度成立 — 根因不是清單漏了哪一項, 而是比對的對象是拼法而不是位址. 現以 `normalizeHost()` 先把值送進 WHATWG URL parser 正規化 (Node core 裡唯一現成的 IPv6/IPv4 正規化器, 會把 `::0`、`0:0:0:0:0:0:0:0`、`::` 收斂成同一個字串), 集合改存正規形式. 往清單裡補上漏掉的拼法被否決: 那是用一份更長的清單再犯一次同樣的錯. 正規化同時套用到三個檢查而非只有出錯的那一個, 因為該模組自己的檔頭就寫著「同一道防護的第二份實作, 是修好第一份之後仍然存在的漏洞」. 一併修好同類的反向缺陷: `--host 0:0:0:0:0:0:0:1` 原本會被當成非 loopback 而拒絕綁定, 那是守衛在反對一種拼法而不是在反對一次曝險. 無法解析的 Host header 仍在正規化之前就被既有的 regex 閘門擋下.
|
|
84
|
-
- **`tospec archive ""` 會對互動選單剛列出的目錄丟出名稱格式錯誤**: 判斷名稱來源的 `nameFromArgument` 比對的是 `changeName !== undefined`, 於是空字串被算成「使用者提供了名稱」, 而後續的 `!changeName` 仍然把它送進互動選單. kebab 格式守衛接著就對選單剛回傳的目錄執行 — 而那道守衛自己的註解寫明它只該作用於參數路徑, 因為選單列出的是磁碟上真實存在的目錄, 其中有些早於這條約束 (`tospec migrate` 逐字複製 OpenSpec 的目錄名, snake_case 在那裡是慣例). 這是 0.19.0-beta.3 修好「守衛跑在選單結果上」之後, 由同一道守衛的另一個入口重新製造出來的. 現改為 `typeof changeName === 'string' && changeName.length > 0`: 空字串是沒有提供名稱, 不是提供了一個空的名稱.
|
|
85
|
-
- **ticket 無法讀取時, 歸檔會靜默丟失 Related Decisions 區段**: `appendDecisionLinks` 為了偵測 ticket 自身的換行慣例而先讀檔 (這是 0.19.0-beta.3 修正 CRLF 混用時加上的), 但讀取失敗時直接 `return`. `appendFile` 從頭到尾不需要讀取權限, 所以這個改動把一條原本能運作的路徑 — ticket 可附加但不可讀回 — 變成 Related Decisions 永久遺失, 而歸檔仍回報成功. 換行慣例是外觀問題, 決策連結是資料; 現在讀取失敗改為回退到 `\n` 並照常附加.
|
|
86
|
-
- **`--tools none` 不再清理退役的 skill 目錄**: `removeLegacySkillDirs` 在重構中被移進 `if (tools.length > 0)` 條件內. 但清除 tospec 自己在舊版產生的目錄, 與這次執行是否寫入新內容無關 — 被關進條件後, `--tools none` (使用者用來停止 tospec 寫入 agent 內容的方式) 反而讓 `.agents/skills/tospec-verify` 這類退役 workflow 留在磁碟上, 繼續被每個讀取 `.agents/skills/` 的 agent 載入, 與使用者的意圖正好相反. 現移回條件之外.
|
|
87
|
-
- **清空選單或 `--tools none` 會印出成功橫幅卻不說明什麼都沒寫**: 成功訊息的每一個區塊都以「選取非空」為前提, 於是空選取的執行會印出 `tospec Setup Complete`、config 行, 以及一行叫使用者執行 `tospec-propose` 的提示 — 而那是一個並不存在於磁碟上的 skill. 接受空選取是 `20260910_214500` 的決定, 沒有問題; 把它回報成一次普通的成功才是問題, 尤其清空選單是很容易誤觸的操作. 現會明說沒有寫入任何 skill、command 或 rule, 且不再建議一個不存在的 skill.
|
|
88
|
-
- **相依關係提示印給了沒有選到該工具的人, 且印在無法影響選擇的時機**: `discloseToolDependencies` 迭代整張 `AI_TOOLS` 並由 `execute()` 無條件呼叫 (僅 `--tools none` 例外), 所以 `tospec init --tools agents` 會印出「Claude Code 會把 skill 連結進 .agents/skills/, 因此選它也會一併選取 Agents」— 對方既沒有安裝 Claude Code, 也沒有任何選單可被這句話影響. 該方法自己的註解已經寫出理由: 這段說明存在的意義是「在使用者回答之後才抵達的解釋無法影響答案」, 而該前提只在有提示可回答時成立. 現改由互動選單路徑在提出問題前呼叫.
|
|
89
|
-
- **專案自有的 `.codex/rules/` 檔案會被誤判為 tospec 殘留**: `hasAnyRuleFile` 計算 `.codex/rules/` 底下任何位置的任何檔案, 但它觸發的提示宣稱那些檔案是 tospec 的產物, 並指名 `.codex/rules/tospec/` 為可刪除的對象. 於是一個從未執行過舊版 tospec、只是自己在 `.codex/rules/team-style.md` 放了規範的專案, 每次 `init` 與 `update` 都會收到這則提示, 而它指名的路徑並不存在. `writeToolRules` 只寫入 `rules/tospec/` 之下, 掃描範圍現與之對齊 — 與 skill 那一半早已限定 `tospec-` 前綴的做法一致, 也才符合該模組自己在下一段就寫明的承諾: 目錄單純存在並不構成殘留.
|
|
90
|
-
- **通過的 `tospec validate` 仍把診斷訊息寫到 stderr**: findings 刻意印在 verdict 分支之外, 讓通過項目上的 WARNING 依然可見 (只在失敗時印等於把降級實作成刪除, 這是 0.19.0-beta.1 的既有決定). 但它們一律走 `console.error`, 於是 `tospec validate <id>` 可能以狀態碼 0 結束卻留下非空的 stderr. pre-commit hook、檢查該串流的 CI 步驟, 以及解析輸出的 agent 都會把它讀成失敗 — 同一個降級換條路徑再被實作成刪除一次, 而且這次還附帶一次假警報. 現在每個項目的 findings 跟著自己的 verdict 走同一條串流 (bulk 模式亦同), 狀態碼 0 即代表 stderr 為空.
|
|
91
|
-
- **`resolveToolSelection` 遇到相依環會產出錯誤的順序而不是拒絕**: 守衛在重新走到「正在拜訪中」的節點時提早 `return`, 等於吸收掉環而不是回報它, 而且仍然產出一份清單 — 由於內層呼叫先 push, 那份清單會把相依者排在它的相依對象之前. 呼叫端正是依照回傳順序寫入目標, 為的就是讓 canonical 目錄先存在再從中複製, 因此這份靜默的結果會讓它從一個尚未寫入的來源去產生工具目錄. 沒有任何順序能滿足一個環, 所以錯的是資料表本身, 必須在編輯發生的地方就說出來: 現改為丟出例外. 目前出貨的 `AI_TOOLS` 無環, 另有一項測試斷言這件事, 因此這道守衛保護的是未來新增的列而非現存狀況.
|
|
92
|
-
|
|
93
|
-
### 其他
|
|
94
|
-
|
|
95
|
-
- **TypeScript 7.0.2 與 vitest 5.0.0 首次在確實安裝的狀態下通過驗證**: `pnpm-lock.yaml` 自 0.19.0-beta.5 起已 pin 住這兩個 major 版本, 但本版開工時 `node_modules` 實際仍是 typescript 5.9.3 與 vitest 4.1.10, 且 `pnpm install --frozen-lockfile` 判定兩者不相容、需要先清空 `node_modules`. 專案沒有 CI, 沒有任何地方會察覺這段落差. 補上一個陷阱: 該次 `pnpm install` 因為沒有 TTY 可確認清空動作而放棄安裝, 卻以狀態碼 0 結束 — 任何以離開碼為唯一判準的腳本都會被它騙過; 需要 `CI=true` 或 `--config.confirmModulesPurge=false` 才會實際執行. 對齊 lockfile 後重跑, `pnpm build` 在 tsgo 原生版 TypeScript 7.0.2 下通過, 測試在 vitest 5.0.0 下 1017 passed / 1 skipped (80 個檔案), 原始碼與 `vitest.config.ts` 均無需為此修改.
|
|
96
|
-
- **兩項回報經查證後判定不需修改**: 其一是 `resolveArtifactOutputsAsync` 與同步版重複三個分支的疑慮 — `outputs.test.ts` 已有 parity 測試, 對非 glob、單層 wildcard 快速路徑、fast-glob 回退、前綴不存在四個分支各自斷言兩者結果相同, 漂移已被守住; 且它宣稱要消除的停頓在它宣稱的路徑上並不存在 (`collectOverview` 只經由同步的 `artifactOutputExists` 與 `listTasksForChange` 觸達本模組, 而 `dashboard-data.ts` 記錄的量測是 30 個 change 下最長停頓 2.1ms, 那正是用同步版量到的). 真正錯的是 docstring 指錯了服務對象, 已改正並註明任何新行為都必須加入 parity 清單. 其二是 `metrics-refresh-atomic.test.ts` 的 temp 目錄洩漏 — 清理實際存在於兩個測試各自的 `finally` 區塊, 實測執行前後 temp 目錄零殘留.
|
|
97
|
-
- **審查修正立為 issue change 追蹤**: `tospec/changes/code-review-fixes/`, 14 項發現逐一記錄處理方式, 含上述兩項「查證後不修改」的理由. `CLAUDE.md` 的 generated-files 段落同步更新, 說明工具目錄是複製而非連結; 其中提到的三個手寫 skill 目錄 (`grill-me` / `grill-with-docs` / `grilling`) 已在 0.19.0-beta.5 退役, 該段敘述一併更正.
|
|
98
|
-
- 上游 OpenSpec 參考點仍停在 `e062b95` (1.12.0), 本版與上游比對無關.
|
|
99
|
-
|
|
100
|
-
## [0.19.0-beta.5] - 2026-09-11
|
|
101
|
-
|
|
102
|
-
**預發布版本**: 需以 `npm install @seanmars/tospec@beta` 明確指定才會安裝, `latest` 仍指向 0.18.0.
|
|
103
|
-
|
|
104
|
-
本版本是一次例行的相依套件更新: 依 npm-check-updates 回報的六個過時套件逐一升級, 三個 patch/minor 沒有引出任何相容性問題, 兩個 major 版本 (vitest、@inquirer/core) 也只是換了版號, 唯獨 TypeScript 5.9.3 直接跳到 7.0.2 這一步, 暴露出套件本身重構帶來的相容性落差. 六個套件刻意逐一升級並各自跑過完整 build 與 test, 而非一次套用全部版本再統一驗證, 使得唯一出狀況的那一個能被準確歸因, 不必事後在六個同時變動的套件裡回溯.
|
|
105
|
-
|
|
106
|
-
### 變更
|
|
107
|
-
|
|
108
|
-
- **六個過時相依套件依 npm-check-updates 報告逐一升級並驗證**: `marked` 18.0.6 → 18.0.12, `@inquirer/prompts` 8.5.2 → 8.7.2, `zod` 4.4.3 → 4.6.2 (皆為 patch/minor, 未觀察到任何行為變化), 以及三個 major: `@inquirer/core` 11.2.1 → 12.0.3, `vitest` 4.1.10 → 5.0.0, `typescript` 5.9.3 → 7.0.2. 每次只調整一個套件, 立即跑 `pnpm build` 與 `pnpm test` 確認 998 passed / 1 skipped (80 個檔案) 不變, 再進入下一個.
|
|
109
|
-
- `@inquirer/core` 升級前, 直接相依其實已經與傳遞相依不同步: `@inquirer/prompts` 8.7.2 底下的 `@inquirer/input` 等子套件已要求 `@inquirer/core` `^12`, 專案自己宣告的卻仍是 `^11.2.1`, 使 `node_modules` 內同時存在兩份互不相容的 `@inquirer/core`. 升級後傳遞相依收斂回單一版本 (減少 4 個套件), 這次升級因此也是在修一個既有的相依漂移, 不只是取得新版號.
|
|
110
|
-
- `@types/node` 另補到 24.13.4, 純型別定義更新, 不影響執行期行為.
|
|
111
|
-
- `pnpm` 的供應鏈保護機制 (`minimumReleaseAge`) 認定 `zod@4.6.2` 發布時間過近, 自動在 `pnpm-workspace.yaml` 加入 `minimumReleaseAgeExclude` 例外項才放行安裝, 屬於 pnpm 自身的預期行為.
|
|
112
|
-
|
|
113
|
-
### 修正
|
|
114
|
-
|
|
115
|
-
- **`build.js` 的 tsc 路徑解析在 TypeScript 7 下丟出 `ERR_PACKAGE_PATH_NOT_EXPORTED`**: `runTsc` 原本以 `require.resolve('typescript/bin/tsc')` 取得執行檔路徑, 而 TypeScript 7 的 `package.json` `exports` map 不再對外公開 `./bin/tsc` 這個 subpath (只有 `bin` 欄位仍指向它) — Node 一旦看到 `exports`, 就把它當成允許清單, 未列出的 subpath 一律視為不存在, 於是一條先前正常運作的路徑, 在編譯行為本身完全沒變的情況下失效. 改為經由仍公開的 `./package.json` 取得套件根目錄, 再以檔案系統路徑手動組出 `bin/tsc`, 不再依賴該 subpath 是否被 exports map 收錄. tsc 對 `src/**/*` 的型別檢查結果不變, 沒有任何原始碼因這次升級而修改.
|
|
116
|
-
|
|
117
|
-
### 其他
|
|
118
|
-
|
|
119
|
-
- **本版未立為 sdd change, 也未新增 ADR**: 六個套件的版號調整與一處相依套件重構後的路徑解析修正, 屬於例行維護, 沒有推翻或新立任何架構取捨. 驗證涵蓋 `pnpm build`、`pnpm test`、`pnpm skills`/`pnpm templates` 兩支產出腳本, 以及對本專案自身 `tospec/` 目錄執行 `tospec validate --all` 與 `--version`/`--help` 的 CLI 手動驗證. 上游 OpenSpec 參考點仍停在 `e062b95` (1.12.0), 本版與上游比對無關.
|
|
120
|
-
|
|
121
|
-
> **補記 (0.19.0-beta.6 加註)**: 上面這段與本節開頭的「例行相依套件更新」描述涵蓋不全. 本版實際還出貨了 `bd45c76 feat(init)!`, 該 commit 以常駐的 `.agents` 目標取代 Codex 寫入目標並改用連結交付 skill, 是一次破壞性變更; 隨附的 `agents-tool-target` change 已歸檔, 手寫的 grill skill 系列一併退役, 並新增兩份 ADR (`20260910_201813-codex-tool-replaced-by-agents-dir.md`、`20260910_214500-agents-listed-target-linked-delivery.md`), 因此「未立為 sdd change, 也未新增 ADR」在事實上不成立. 本條目依慣例保留原文不改寫, 缺漏的部分改由此註記與 0.19.0-beta.6 的條目說明; 其中的連結交付機制已於 0.19.0-beta.6 推翻.
|
|
122
|
-
|
|
123
|
-
## [0.19.0-beta.4] - 2026-09-10
|
|
124
|
-
|
|
125
|
-
**預發布版本**: 需以 `npm install @seanmars/tospec@beta` 明確指定才會安裝, `latest` 仍指向 0.18.0.
|
|
126
|
-
|
|
127
|
-
本版本只改一份檔案的正文: 隨套件發布、載入到 agent 的 workflow 規則模板. 0.19.0-beta.3 已經把它縮減為正確的兩條原則, 這次要處理的不是它要求什麼, 而是它怎麼被讀 — 一份以權威身分載入 agent 的文字, 若只寫命令不寫理由, 它能約束的就只有它字面涵蓋到的情境.
|
|
128
|
-
|
|
129
|
-
### 變更
|
|
130
|
-
|
|
131
|
-
- **workflow 規則模板改寫為結構化英文, 每條規則附上自己的失效機制**: 模板正文原本是兩條 bullet, 第一條英文、第二條中文, `IMPORTANT:` 夾在第一條的句子中間, 兩條都只給命令不給理由. 混語言與埋沒的強調只是表面症狀; 成因在於這份檔案的身分 — 它是被當成權威來源載入 agent 的那一份文字, 而沒有理由的命令在邊界情境無法自我防守: agent 讀得懂「除非必要」的字面意思, 卻推不出違反它會發生什麼, 於是「必要」的門檻實際上由當下的方便程度決定. 第一條還有一個更具體的問題: 「以程式碼為準」沒有指出方向, 而 tospec 自身就內建 `/tospec-sync` 這種把規格對齊程式碼的流程, 「文件與程式碼不一致」因此很容易被讀成「該修程式碼讓它符合文件」, 恰好是這條規則要禁止的那一半. 現改為兩個具名 section, 各自寫出規則、可執行的行為 (回報落差而非改程式碼去迎合文件; 斷言前先在程式碼中驗證) 與失效機制 (文件記錄的是撰寫當下的意圖, 會隨程式演進漂移; 歸檔目錄不隨程式更新), 並把優先序提到標題下方獨立一行, 明講它高於文件、註解、規劃產物與先前對話. 只把中文那條翻成英文的最小改法被否決: 那修掉了語言不一致, 卻原樣留下真正會失效的部分. 規則語意一字未變, `agent-workflow-rules` 規格要求的三件事 (程式碼優先、預設不讀兩個 archive 目錄、只保留原則而不複製各 skill 的程序) 全數保留, 因此本次沒有 delta spec. 既有產出檔的正文仍符合自身記錄的雜湊, 下次 `init` / `tospec rules` / `update` 會判定為正常升級而非衝突, 使用者無須手動處理.
|
|
132
|
-
|
|
133
|
-
### 其他
|
|
134
|
-
|
|
135
|
-
- **本版未立為 change, 也未新增 ADR**: 變更落在 `agent-workflow-rules` 既有規格的框架之內, 是措辭與結構的改寫, 沒有推翻或新立任何取捨, 因此 `tospec/decisions/` 自 0.19.0-beta.3 的三份 ADR 之後沒有新增. 本專案自身的 `.claude` / `.codex` 規則檔已由 `tospec rules` 重新產生, 與 `assets/` 模板同步. 上游 OpenSpec 參考點仍停在 `e062b95` (1.12.0), 本版與上游比對無關. 測試 959 passed / 1 skipped (78 個檔案).
|
|
136
|
-
|
|
137
|
-
## [0.19.0-beta.3] - 2026-09-10
|
|
138
|
-
|
|
139
|
-
**預發布版本**: 需以 `npm install @seanmars/tospec@beta` 明確指定才會安裝, `latest` 仍指向 0.18.0.
|
|
140
|
-
|
|
141
|
-
本版本把 0.19.0-beta.2 出貨時明列的七項缺陷全數修掉, 並替換掉同一版才引入的 workflow 規則正文. 七項缺陷沒有共同的機制, 只有共同的發現方式: 它們是審查 `main...HEAD` 時逐行讀出來的, 測試套件一項都沒有覆蓋到. 但它們確實共有一種形狀: **守衛檢查的內容是對的, 位置是錯的** — 檢查跑在互動選單覆寫過的變數上、掃描跑在比目標更大的索引上、路徑推導對歸檔目錄回答了一個不存在的 change 名稱、opt-in 只抵達兩道防護的其中一道. 每一個守衛單獨看都成立, 放在邊界的錯誤一側就什麼都沒有保護到. 而 beta.2 之所以帶著它們出貨, 是因為當時只把它們記成一份 issue change 與一支 probe script: 明列已知缺陷不等於處理它們.
|
|
142
|
-
|
|
143
|
-
### 變更
|
|
144
|
-
|
|
145
|
-
- **workflow 規則模板改為兩條原則, 不再重述各 skill 的階段邊界**: 0.19.0-beta.2 引入的模板正文是七條分階段指引 (先讀既有 specs 與 ADR、規劃只產出文件、實作依已確認 tasks、決策走 tospec-decision、同步不得掩蓋缺漏、歸檔前完成驗證、不強制所有開發先走 tospec), 現改為兩條: 程式碼為唯一事實來源, 以及除非必要或使用者明確要求, 否則不讀取 `tospec/changes/archive` 與 `tospec/tickets/archive`. 縮減的理由不是那七條寫錯了, 而是它們每一條都已由各 skill 自身的指引定義並強制, 規則檔重述它們等於製造第二份需要同步的副本 — 而規則檔正是被當作權威來源載入 agent 的那一份, 一旦 skill 文案調整, 它會靜默過時且沒有任何機制會發現. 規則檔真正要解決的問題是 agent 把過時文件當成現況事實, 尤以已歸檔的變更與 ticket 為甚: 它們不隨程式演進更新, 卻與現行文件同樣會被語意檢索命中, 而分階段程序指引解決不了這件事. 一併承認 beta.2 記下的顧慮 (「避免把 code wins 誤用為範圍授權」) 被推翻: 該護欄改由 sync 與 archive 兩個 skill 的既有指引承擔. 保留七條再疊上程式碼優先的折衷被否決 — 那不必推翻任何理由, 但留下的正是本次要移除的那份會過時的副本. 既有產出檔的正文仍符合自身記錄的雜湊, 因此下次 `init` / `tospec rules` / `update` 會判定為正常升級而非衝突, 使用者無須手動處理. 決策記錄: `tospec/decisions/20260910_142708-workflow-rule-template-source-code-precedence.md`.
|
|
146
|
-
|
|
147
|
-
### 修正
|
|
148
|
-
|
|
149
|
-
- **`tospec archive` 的名稱格式檢查跑在互動選單的結果上**: 該守衛自己的註解寫明它只該作用於參數路徑, 因為互動選單列出的是磁碟上真實存在的目錄, 其中有些早於 `KEBAB_ID_REGEX` 這條約束. 程式碼卻把檢查放在 `changeName = selectedChange` 之後, 對同一個變數執行 — 於是選單前一秒才列出來的 snake_case change, 參數與選單兩條路徑都歸不了檔. 這不是罕見形狀: `tospec migrate` 逐字複製 OpenSpec 的目錄名, 而 snake_case 在那裡是慣例. 修正改為在互動分支有機會覆寫名稱之前先記下來源. 把整段檢查上移也能修好, 但會讓下一位讀者只能從敘述順序反推意圖, 而註解已經把意圖寫下來了.
|
|
150
|
-
- **wildcard 綁定加上 `--allow-remote` 會 403 掉每一個連進來的 client**: `assertBindableHost` 把 `--allow-remote` 讀成「已接受網路曝險」, 而 `isAllowedHostHeader` 從頭到尾只對綁定位址做字面比對. 沒有任何 Host header 能字面等於 `0.0.0.0`, 於是 `tospec dashboard --host 0.0.0.0 --allow-remote` 印出 URL、在每個介面上監聽, 然後拒絕每一個真的抵達的請求. 成因是那個 opt-in 只抵達了兩道防護的其中一道, 而使用者觀察到的是一個壞掉的 dashboard, 不是一次被拒絕的 host — 這正是靜默失敗最貴的地方. 現由啟動時解析一次的 `acceptsAnyHostHeader` 把 opt-in 帶到 Host 檢查面前; loopback 與具體位址的 rebinding 防護一字未動. 不在 predicate 內部把 `0.0.0.0` 正規化為「任意」: 它看不到旗標, 那等於把「wildcard 就是信任所有人」埋成一個在呼叫端完全看不見的假設. 決策記錄: `tospec/decisions/20260910_150312-allow-remote-wildcard-host-policy.md`.
|
|
151
|
-
- **delta spec 裡重複的一般標題被當成重複的 delta 區段擋下**: `findDuplicateSections` 掃的是未經過濾的一般區段索引, 於是一份 delta spec 寫了兩個 `## Purpose` 就會以「無法判斷哪些 requirement 屬於哪一段」的訊息驗證失敗 — 對一個不擁有任何 requirement 的標題而言這句話是假的, 而它足以擋下一份 delta 內容完全合法的檔案歸檔. 這是 0.19.0-beta.1 修正重複區段靜默遺失時一併引入的迴歸: 新增的拒絕路徑正是沒有人會走的那一條, 所以過寬的掃描範圍出貨時無人察覺. 掃描現限縮為四個 delta 標題, 沿用 parser 本來就在消費的那組名稱; 改為過濾 `splitTopLevelSections` 被否決, 那是一個通用索引, 其他呼叫端依賴它的完整性.
|
|
152
|
-
- **dashboard 的 `POST /api/task` 可以改寫已歸檔 change 的 checkbox**: `changeNameFromTaskFile` 假設路徑形狀為 `tospec/changes/<name>/`, 對已歸檔的 change 因此回傳字面字串 `archive`. 凍結檢查接著去找一份在那個路徑下不可能存在的 `sync-report.md`, 讀到 null, 便放行了寫入 — 於是經 sync 認證、理應永久不變的歸檔內容可被就地編輯. 現在 `changes/archive/` 底下的寫入一律直接拒絕, 且擋在 sync 閘門之前, 因為已歸檔的 change 是「已經不可變」而不是「即將變得不可變」, 讓它去走一次凍結判定等於承認那個判定有可能放行. 在 `resolveTospecFile` 拒絕該前綴則被否決, 範圍過大: `/api/render` 共用同一個 helper, 而它確實應該以唯讀方式呈現歸檔文件.
|
|
153
|
-
- **metrics 重新整理失敗時, 會用舊的擷取時間標示新的數字**: 重新整理把兩份快取與 `capturedAt` 各自從自己的 await 直接指派出去. 第二次讀取拋錯時, 第一份快取已經換上新值而時間戳還停在舊的, 之後每一個請求都送出標著過期擷取時間的較新數字 — 正是 `capturedAt` 在 0.19.0-beta.1 被引入時要消除的那種歧義, 只是這次由失敗路徑重新製造出來. 兩次讀取現在都先落在區域變數, 三個欄位一起提交. 讓它正確的不是 `Promise.all`, 而是一起提交.
|
|
154
|
-
- **`tospec skill-metrics` 的綁定拒絕訊息指名一個它並不接受的旗標**: 訊息結尾寫著 `Pass --allow-remote`, 而 `skill-metrics` 從未註冊這個旗標 — 唯一無法照著建議行動的指令, 正是收到這個建議的那一個. 審查原本推論「這個守衛是死碼, 刪掉即可」, 該推論被推翻: `startMetricsServer` 是匯出的且接受 host, 守衛在程式化路徑上是活的, 刪除它會移除一項真實的檢查. `assertBindableHost` 現改由呼叫端提供補救文字, 沿用它對 `serverName` 已經在用的模式, 使守衛本身不再含有任何旗標名稱 — 第三個 local server 因此無法繼承一句它兌現不了的建議. 為求對稱而替 `skill-metrics` 補上 `--host` / `--allow-remote` 被否決: 那是為了讓一句話成真, 而擴大 transcript 衍生資料的曝險面. 決策記錄: `tospec/decisions/20260910_154657-metrics-server-stays-loopback-only.md`.
|
|
155
|
-
- **格式錯誤的 spec id 在 `/api/specs` 回 500 而非 400**: `handleChangeDetail` 為它的 decode 加了守衛並回 400; `handleSpec` 做的是同一個 decode 卻沒有守衛, 於是 `%zz` 拋出的 `URIError` 以 500 帶著原始錯誤字串浮上來. 兩者早已分岔, 而一則註解還宣稱它們「刻意完全相同」 — 註解描述的是撰寫當下的意圖, 沒有任何東西強制它繼續為真. 兩者現在共用一個 `decodePathSegment`. 只修 `handleSpec` 也能還原那個不變式, 但會讓同一則註解有機會第二次過時.
|
|
156
|
-
|
|
157
|
-
### 其他
|
|
158
|
-
|
|
159
|
-
- **beta.2 明列的七項缺陷以 `branch-review-defects` 立為 issue change 並已歸檔**: 歸檔為 `20260910_164546-branch-review-defects`, sync 判定兩條 delta Requirement 皆為 MATCH 並將 `local-server-host-policy` 併入 `tospec/specs/`. 七項中有五項改變了可觀察行為卻沒有對應的 delta spec (選單名稱守衛、重複區段掃描範圍、歸檔寫入拒絕、原子化重新整理、格式錯誤 id 的狀態碼); 依規則這些一律回報而非事後補寫規格, sync-report 已據實記錄. 隨 beta.2 附上的 `probe.mjs` 隨該 change 一併歸檔, 七項斷言現已全綠. `agent-workflow-rules` 也在本版完成歸檔 (`20260910_142926-agent-workflow-rules`), 補上了 beta.2 出貨時尚缺的 sync.
|
|
160
|
-
- **三份新 ADR, 全部來自本版**: `20260910_142708-workflow-rule-template-source-code-precedence` (模板正文縮減, 取代前一份 ADR 的模板內容段, 其七項機制決策不受影響)、`20260910_150312-allow-remote-wildcard-host-policy`、`20260910_154657-metrics-server-stays-loopback-only`. 上游 OpenSpec 參考點仍停在 `e062b95` (1.12.0), 本版與上游比對無關. 測試 959 passed / 1 skipped (78 個檔案).
|
|
161
|
-
- **儲存庫根目錄整理**: `how-to-publish.md` 移入 `docs/`, 內容未改; 刪除 `report.md` (v0.11.0 對 `output/` 的 skill 稽核) 與 `update.md` (對 2026-08-14 某個 commit 的上游比對分析) 兩份已完成任務的工作筆記. 兩者沒有任何東西引用, 而一份釘在舊上游 ref 的分析比沒有分析更糟 — 它讀起來像是現況. 選擇刪除而非移進 `docs/`: 需要那些推理時 git 歷史仍然握有它們.
|
|
162
|
-
|
|
163
|
-
## [0.19.0-beta.2] - 2026-09-10
|
|
164
|
-
|
|
165
|
-
**預發布版本**: 需以 `npm install @seanmars/tospec@beta` 明確指定才會安裝, `latest` 仍指向 0.18.0.
|
|
166
|
-
|
|
167
|
-
本版本只做一件事: 讓 agent 的 workflow 規則從「init 寫一次就再也回不去的檔案」變成套件能持續更新的產出物. 原本的規則正文是一個寫死在 TypeScript 裡的字串, 只在 init 途中寫出一次 — 而 init 是為「還沒有 tospec 的專案」設計的, 於是套件之後改了規則, 既有專案沒有任何指令拿得到. 一旦要讓它可更新, 下一個問題立刻成立且無法迴避: 產出檔與被使用者改過的檔案在磁碟上長得一模一樣, 而更新規則的每一種天真做法, 不是每次升級都誤判為衝突, 就是每次升級都靜默輾過使用者的修改.
|
|
168
|
-
|
|
169
|
-
### 新增
|
|
170
|
-
|
|
171
|
-
- **共用 workflow 規則模板, 以及新的 `tospec rules [path]` 入口**: tospec 過去只在 init 寫入一份固定的 `rules/tospec/decision.md`, 除此之外沒有第二份規則的容身處, 也沒有任何入口能在套件升級後補寫或刷新它. 成因不是缺少一個指令, 而是正文的存放位置: 字串常數只能經由「重跑 init」抵達磁碟, 而那條路徑本身帶著建立整個專案佈局的副作用, 沒有人會為了拿一份規則去走它. 現在正文獨立為套件內的 `assets/rules/tospec/single-sourc-of-truth.md` 隨 npm 發布, 由 `init`、新增的 `tospec rules` 與既有的 `update` 三個入口共用, 寫入 `.claude/rules/tospec/single-sourc-of-truth.md` 與 `.codex/rules/tospec/single-sourc-of-truth.md`, 與既有的 `decision.md` 並存, 不遷移也不合併 — 這次要新增的是另一份規則, 而搬動一份已經在使用者專案裡的檔案是獨立的取捨, 不該搭這班車. 正文不隨專案 schema 或已安裝的 workflow 變動, 也不開放專案覆寫: 內容由維護者持有正是這次的需求, 而覆寫機制等於為了一個還沒有人提出的問題, 先引入一組優先序規則. `tospec rules` 只處理已安裝 tospec skills 的 agent, 且完全不碰 skills、commands 與設定 — 單純存在一個 `.claude/` 目錄不代表該專案要 tospec 的規則. Codex 這次只產生檔案: 該目錄沒有官方的 Markdown 自動載入機制, 與其代為寫入 `AGENTS.md` 或 `config.toml` 製造一個看似會生效的設定, 不如在輸出裡明說載入需自行處理. 決策記錄: `tospec/decisions/20260910_001756-agent-workflow-rules.md`.
|
|
172
|
-
- **產出檔內嵌版本標記與內容雜湊, 用來分辨套件升級與使用者手改**: 規則一旦可以被更新, 覆寫就成了問題 — 「內容與當前 template 不同」同時是這兩件事的樣子, 以它為判準必然二選一地錯: 要嘛每次套件升級都被當成衝突擋下, 要嘛使用者的每一次修改都在下一次 `update` 靜默消失. 根本原因是判準問錯了對象: 該問的不是「它跟現在的 template 一不一樣」, 而是「它還是不是一份沒被動過的產出」. 現在每份產出在框架標記中記錄自身正文的 SHA-256, 只要正文仍與它自己記錄的雜湊相符, 就是未經手改的產出 — 無論它出自哪一版 template, 都能安全換上新版; 正文被改、標記外多出內容、標記缺失、重複或格式錯誤, 則一律視為衝突並原樣保留. 選擇檔內自證而非在外部保存一份「上次產出了什麼」的紀錄: 後者能讓產出維持純文字, 但代價是第二份狀態, 而當它與檔案本身失去同步時, 沒有任何東西能判斷哪一邊才是對的. 比對前先正規化行尾, 因此以 CRLF 檢出的檔案不會被誤判為手改.
|
|
173
|
-
- **規則衝突在任何專案寫入之前整次擋下**: 三個入口都先對所有目標建立唯讀計畫, 任一目標衝突就整次失敗, 此時 skills、commands、設定、legacy 清理與另一個 agent 的規則全部尚未被動過. 若改為「輪到哪個 agent 才檢查它自己」, 第二個 agent 的衝突會留下第一個 agent 已經更新的檔案, 使用者拿到的是一個沒有任何指令描述得出來的中間狀態, 而重跑會再走一次同樣的部分更新. init 為此把兩件既有的寫入動作 — legacy user state 遷移, 以及以實際寫入探測權限的 validate — 移到預檢之後: 「衝突時不寫入任何專案檔案」這句話, 若把探測性的寫入排除在外就不成立. `--force` 不略過衝突檢查, 也不提供強制覆寫旗標: force 目前的意思是「即使產出已是最新也重新產生一次」, 而不是「丟棄使用者的修改」, 讓一個旗標同時代表這兩件事, 會使它在最該謹慎的時候最危險. 解除衝突的方式是自行保留修改內容、刪除該規則檔後重跑.
|
|
174
|
-
|
|
175
|
-
### 其他
|
|
176
|
-
|
|
177
|
-
- **本版的變更以 `agent-workflow-rules` 立為 sdd change, 尚未歸檔**: 五組任務全數完成且測試全綠 (929 passed), 但 change 仍留在 `tospec/changes/`, 歸檔前的 sync 留待下次. 隨附一份 ADR (`20260910_001756-agent-workflow-rules.md`), 是自 0.19.0-beta.0 以來的第一份. 上游 OpenSpec 參考點仍停在 `e062b95` (1.12.0), 本版與上游比對無關.
|
|
178
|
-
- **另記錄了七項本版尚未修正的缺陷**: `branch-review-defects` 這個 issue change 記下針對 `main...HEAD` 的審查結果, 並附一支 `probe.mjs` 作為回饋迴圈 (每項斷言的是**正確**行為, 因此 `FAIL` 代表缺陷仍在). 出貨當下七項全紅, 所以它們在 0.19.0-beta.2 中依然存在. 在此明列是因為其中兩項會影響使用者觀察得到的行為: dashboard 的 `POST /api/task` 仍可改寫已歸檔 change 的 checkbox, 而 `tospec skill-metrics` 的繫結拒絕訊息會指名一個它並不接受的 `--allow-remote` 旗標.
|
|
179
|
-
|
|
180
|
-
## [0.19.0-beta.1] - 2026-09-08
|
|
181
|
-
|
|
182
|
-
**預發布版本**: 需以 `npm install @seanmars/tospec@beta` 明確指定才會安裝, `latest` 仍指向 0.18.0.
|
|
183
|
-
|
|
184
|
-
本版本不跟進上游, 而是一次針對自身程式碼的稽核. 五組修正共用同一個形狀: **規則早已寫在別處, 只是從未被抵達** — 「改寫檔案時保留其原本的行尾」寫在 CLAUDE.md, 「寫入任何一份之前先驗證每一份重建的規格」寫在該函式自己的註解, 「第二份實作就是一個洞」寫在 `local-server.ts` 的模組開頭, 「同一個 requirement 名稱只會出現一次」則是兩個資料結構各自的假設, 而沒有任何一層負責執行它. 這些不是被判斷為不適用而略過的規則, 而是根本沒有程式碼走到它們面前; 徵兆也因此一致: 成功路徑寫得異常小心的模組, 在自己的失敗路徑與改寫路徑上放棄了同樣的標準, 而指令仍然回報成功.
|
|
15
|
+
- **archive 新增 artifact 完整性閘門**: sdd schema 標為必要的 proposal / design / tasks 缺任一份即以 `archive_artifacts_incomplete` 拒絕, 可用 `--yes` 覆寫.
|
|
16
|
+
- **delta 裡未填寫的 template placeholder 是 ERROR**: `[name]`、`[old name]`、`[scenario name]` 這類整段方括號的名稱出現在任何命名處即拒絕, 原封不動的 template 不再通過驗證.
|
|
17
|
+
- **`validate --strict` 的判準與 archive 對齊**: 合併預檢的發現由 INFO 升為 WARNING, 整份仍是 template 的 change 不再回報 valid, HTML 註解不計入區段長度.
|
|
18
|
+
- **`Why` 的長度上下限與 10 個 delta 的上限開始實際生效**: 規則移入 `schema.yaml`, 原本無人呼叫的 `Validator.validateChange` 移除.
|
|
19
|
+
- **artifact 平手順序改依 schema 宣告順序**: `status` 與 `nextSteps` 不再因字母序把 design 排在 specs 之前.
|
|
20
|
+
- **會寫入的指令不再默默建立 tospec root**: `new change` 與 `decision new` 在未初始化的目錄以 `no_tospec_root` 拒絕.
|
|
21
|
+
- **schema 的 `generates` 必須留在 change 目錄內**: 含 `..` 或絕對路徑的 schema 載入時即拒絕.
|
|
22
|
+
- **JSON 輸出形狀調整**: `config list --json` 的設定內容移到 `config` 鍵下; `list --json` 失敗時 `changes` 為 `null` 而非 `[]`; `instructions apply` 的任務編號從 `description` 移到 `id`; `status --json` 以 `optional` (schema 宣告為選用) 與 `skipped` (change 宣告不做) 區分兩種狀態; `archive --json` 一律帶 `totals`.
|
|
23
|
+
- **ticket frontmatter 的 `type` 改為 change-type 詞彙**, schema 另立 `schema:` 欄.
|
|
24
|
+
- **skill 交付機制改變**: `.agents/skills/` 成為 canonical 目錄並列為可選取的工具目標, Claude Code 與 Codex 的 skill 目錄由它複製而來 (不使用連結, 以免提交內容隨作業系統改變), 編輯 canonical skill 後需重跑 `tospec update`. 手寫的 grill skill 系列退役.
|
|
25
|
+
- **`tospec list` 的 `✓ Complete` 欄改名 `✓ Tasks done`**, 它數的只有 checkbox.
|
|
185
26
|
|
|
186
27
|
### 新增
|
|
187
28
|
|
|
188
|
-
- **
|
|
29
|
+
- **delta 文法新增 `## REMOVED Scenarios` 區段**: scenario 終於有刪除與改名的合法路徑; 接受 bullet 與 header 兩種形式, 宣告卻沒對應到任何 drop 是 ERROR.
|
|
30
|
+
- **REMOVED / RENAMED 條目接受 bullet 形式**: 不再強制 `### Requirement:` 前綴, template 與 schema instruction 附上可直接複製的範例.
|
|
31
|
+
- **validate 回報 archive 會拒絕的 delta**: 以 dry-run 執行同一次合併, 回報 MODIFIED / RENAMED 目標不存在、ADDED 撞名、REMOVED 什麼都移除不到, 以及新 capability 缺 `## Purpose` (`DELTA_PURPOSE_MISSING`). 已由結構檢查回報的失敗種類不重複.
|
|
32
|
+
- **MODIFIED 比對 bullet 內容**: scenario 名稱齊全但少了 WHEN / THEN bullet 時以 WARNING 回報, 擋下平行 change 的無聲回退.
|
|
33
|
+
- **validation report 新增 `notes` 欄位**: 關於規則本身的說明 (例如 SHALL/MUST 檢查只認英文) 在報告層印一次, 不再逐 requirement 重複.
|
|
34
|
+
- **`--json` 覆蓋率補齊**: `init`、`update`、`rules`、`migrate` 與 `config` 全部七個子命令都支援 `--json`; 每個命令的失敗 null-shape 集中為單一表格, 由測試全面掃描.
|
|
35
|
+
- **agent 拿到的脈絡更完整**: `status` / `instructions --json` 帶出 `changeMetadata` (goal, decisions), 文字模式帶 `<change_context>`; `instructions` 交付 schema 宣告的驗收條件 (`requiredSections`、`minSectionLength`), 並以 `artifact_blocked` / `artifact_skipped` 在 `status[]` 回報被阻擋與被 `skip_specs` 宣告掉的 artifact.
|
|
36
|
+
- **`rules:` 接受 `apply` 鍵, `instructions apply` 攜帶專案 `context`**: 專案慣例終於抵達真正寫程式碼的階段. apply 另回報 `missingContext`, 任務編號取自檔案內容.
|
|
37
|
+
- **共用 workflow 規則模板與 `tospec rules [path]`**: 規則正文隨 npm 發布, 由 `init` / `rules` / `update` 共用; 產出檔內嵌 SHA-256 標記以分辨套件升級與使用者手改, 任一目標衝突則整次不寫入. 正文縮減為兩條原則 (程式碼為唯一事實來源; 非必要不讀 archive 目錄), 以結構化英文寫出各自的失效機制.
|
|
38
|
+
- **`decision list --reindex`**: 回填 `index.md` 新增的 status 欄、補列缺少的決策 (依時序插入)、回報斷鏈列但絕不刪除; 不帶旗標的 `decision list` 也回報斷鏈.
|
|
39
|
+
- **`config profile custom`**: 三處文件都指向它, 現在真的存在; 未設 workflow 清單時拒絕. 只安裝子集卻被其他 skill 點名的交互引用缺口, 在選定、安裝與檢視時回報.
|
|
40
|
+
- **零星新增**: `templates --json` 的 `exists` 欄位; `list --specs` 逐 spec 回報 `status: ok | unreadable`; `status --all --json` 的失敗 change 同時進 top-level `status`; metrics 頁面標示數據擷取時間 `capturedAt`.
|
|
189
41
|
|
|
190
42
|
### 安全
|
|
191
43
|
|
|
192
|
-
-
|
|
193
|
-
|
|
194
|
-
-
|
|
44
|
+
- **dashboard `/api/render` 不再輸出未淨化的 HTML**: raw HTML 一律轉義, URL scheme 改為解析而非樣式比對, 每個回應帶 `script-src 'self'` CSP.
|
|
45
|
+
- **`/api/changes/<name>` 補上路徑跳脫檢查**: percent-encoded 的 `..` 原本能穿過 URL 正規化; 格式錯誤的 id 回 400 而非 500.
|
|
46
|
+
- **local server 綁定防護統一**: `assertBindableHost` 由 dashboard 與 skill-metrics 共用; host 先經 WHATWG URL 正規化, `::0` 等同義拼法不再繞過 wildcard 判斷; `--allow-remote` 同時抵達綁定與 Host header 兩道防護.
|
|
47
|
+
- **`POST /api/task` 收緊**: 拒絕已歸檔 change 的寫入, 只認 schema 追蹤的 task 檔, symlink 過的檔案回 403.
|
|
195
48
|
|
|
196
49
|
### 變更
|
|
197
50
|
|
|
198
|
-
- **
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
-
|
|
206
|
-
- parser 改回傳 `Map<string, SectionBody[]>`, 鍵為小寫標題, 每次出現各自解析後再串接結果. 保持各次出現分離而非合併其行, 有兩個決定性理由: 合併會跨越作者寫下的邊界捏造結構 — 一個 RENAMED 區段的 `FROM:` 會與另一段的 `TO:` 配對, 發明出兩段都沒有宣告的 rename; 而 `SectionBody.offset` 是單一基準索引, 四個呼叫端以 `offset + i + 1` 推導行號, 串接不相鄰的文件切片沒有合法的基準, 第二段回報的行號會靜默錯誤 — 正是本次要移除的那類缺陷. 逐次解析讓每個純量 offset 維持正確, 四個呼叫端一行未動.
|
|
207
|
-
- 鍵改小寫同時修掉同一種遺失的第二種樣貌: `## added requirements` 原本是另一個物件鍵, 而不分大小寫的查詢從此永遠找不到它. 改用 Map 也讓 `## __proto__` 這個標題落進自有項目, 而不是 `Object.prototype` 上的存取器.
|
|
208
|
-
- 驗證端直接拒絕重複的 delta 區段而非合併它: 兩個相同標題是編輯意外, 合併等於替作者猜測他想怎麼分組. 兩半分工不同 — parser 保證即使繞過驗證也不會遺失 (`archive --no-validate` 是真實存在的旗標), 驗證器則在修改還便宜的時候告訴作者這份檔案壞了.
|
|
209
|
-
- 主規格端新增 `duplicate-requirement` 這個 kind 到 `findMainSpecStructureIssues`, 而非在 `nameToBlock` 迴圈裡拋錯: 該函式本來就在合併前走過目標、本來就回報兩種結構缺陷、也本來就會讓 `buildUpdatedSpec` 在任何寫入之前中止, 因此不需要新的中止機制, 並免費地經由 `applySpecRules` 抵達 `tospec validate`. 放在 Map 迴圈裡的守衛則會在 `findMainSpecStructureIssues` 已經宣告該檔案乾淨之後, 從更低層再開火 — 兩個守衛對同一個檔案各說各話.
|
|
210
|
-
- **改寫檔案時破壞行尾字元與 fenced 區塊內的空行**: CLAUDE.md 已明文寫下這三處違反的規則 — 「改寫檔案時保留其原本的行尾」— 因此這是對既有約定的執行落差, 而非新政策; 它引用的前例也不是假設: 一個 CRLF 的 tasks 檔曾讓 archive 對一個任務其實已完成的 change 回報 `archive_tasks_missing`.
|
|
211
|
-
- 空行收合跑過整份文件. 它存在的理由是 recompose 串接的四段各自帶著邊界換行, 接縫會累積空行; 但 `replace(/\n{3,}/g, '\n\n')` 分不出接縫與作者寫在程式碼範例裡的空行, 於是每一次 archive 都悄悄改動了規格裡的範例. 改為逐行處理並以檔案內既有的 `buildCodeFenceMask()` 遮罩 — 同檔另外兩個函式已經在用它 — 接縫依建構方式必然落在 fence 之外, 遮罩不會損失任何收合原本要做的事.
|
|
212
|
-
- CRLF 的 `spec.md` 被改寫成 LF, 在 Windows 上產生整檔 diff, 把實際只動了一條 requirement 的變更埋掉. 行尾是在管線**前端**被破壞而非寫入時: `extractRequirementsSection` 呼叫 `normalizeDocument`, `normalizeBlockRaw` 又逐塊正規化一次, `recompose` 以 `'\n'` 串接 — 等 `writeUpdatedSpec` 執行時已經沒有東西可保留. 解析前正規化是正確的而且保留 (底下每一條結構 regex 都假設 LF, 而違反該假設正是當初 checkbox 缺陷的成因); 缺陷在於那個約定被丟棄而不是被記錄下來. 現由 `buildUpdatedSpec` 在原始位元組上偵測並回傳, `writeUpdatedSpec` 還原.
|
|
213
|
-
- 採整檔旗標而非 `setTaskDone` 用的逐行保真: 後者改寫的是一份其餘完全未動的檔案中的一行, 每一行的行尾都有定義; 而合併後的規格含有原本不存在的行, 逐行對它們沒有定義, 檔案層級的約定才是唯一涵蓋得了輸出的答案. 因此混合行尾的輸入會統一輸出為 CRLF — 那確實改動了原本沒被碰的那一半, 而這是正確的取捨, 因為另一個選項是替新行憑空發明一個行尾.
|
|
214
|
-
- `appendDecisionLinks` 附加的是硬編 LF 的區塊. `appendFile` 從不讀取檔案, 於是 ticket 自身的約定從未被查詢, 一份 CRLF 的 ticket 會變成標題之上 CRLF、之下 LF. 現在改為先讀取. 鄰居 `retargetTicketRef` 原本就是安全的, 但那是出於建構方式而非出於留意 — 它的 pattern 排除行終止符, 所以 replace 只會在行內改寫 — 該推理現已寫成註解, 以免下一位讀者把它「簡化」進同一個陷阱.
|
|
215
|
-
- **六處錯誤路徑缺陷隱藏或半套用了真實的失敗**: 六個小缺陷都落在「已經出事之後才會執行」的路徑上, 而它們所在的程式碼在成功路徑上異常小心. 合併記錄是因為它們共用這個性質, 其中兩個還共用一個更具體的成因: 守衛被放在邊界的錯誤一側.
|
|
216
|
-
- **archive 的歸檔目錄碰撞檢查跑在主規格寫入之後**: 走到該檢查, 代表規格已合併而 change 仍在 `tospec/changes/` 裡, 於是重試會再套用同一批 delta, REMOVED/RENAMED 接著走它們的「已同步」分支, 計數不再描述現實. 這與同一個函式自己陳述的不變式字面相反 — 「寫入任何一份之前先驗證每一份重建的規格, 使晚期的驗證失敗真的讓所有目標維持不變」. 該檢查只需要一個時間戳與一個名稱, 沒有任何東西迫使它必須晚; 現移到寫入之前, 時間戳只計算一次並沿用於搬移, 使被檢查的目錄就是被建立的目錄. 觸發機率很低 (需要同一秒內對同名 change 執行兩次), 但後果是一次沒有 undo 的半套用合併.
|
|
217
|
-
- **`Change not found` 從它自己的 try 之內拋出**: 該分支是死碼, 暗示一個並不存在的區別; 而那個裸 catch 同時把真正的 IO 失敗改寫成「找不到」— 權限問題被回報成 change 不存在. 改為 `fs.stat(...).catch(...)` 並以既有的 `isMissingPathError` 收窄, 只有真的不存在的路徑才能回答「找不到」. 單用 `.catch(() => null)` 會保留吞噬, 收窄才是重點.
|
|
218
|
-
- **`<name>` 從未檢查形狀**: 帶分隔符的名稱會讓 `changeDir` 指到 `tospec/changes/` 之外, 歸檔目標落在非預期的位置. 這不是安全邊界 — 那是一個已經握有 shell 的人給的 CLI 參數 — 但 `KEBAB_ID_REGEX` 已經定義了合法形狀而這條路徑忽略它, 於是一個打字錯誤換來的是困惑的失敗而不是清楚的訊息. 只在參數路徑驗證: 互動式選單列的是真實目錄, 其中有些早於這條約束, 拒絕一個 CLI 剛剛才提供的選項會讓可歸檔的 change 無法歸檔.
|
|
219
|
-
- **`shared-output` 的 `asStatus` 以裸屬性存取讀 `.diagnostic`**: 於是 `throw null` 或裸的 `Promise.reject()` 會讓那個存取本身拋錯 — 在錯誤處理器之內, 以一個不相關的 TypeError 取代真正的成因, 並完全抑制 JSON 外殼, 而這個 CLI 的契約是每一次 `--json` 失敗都恰好送出一份 JSON. 同檔的 `asErrorMessage` 早就防了這個形狀, 一個 optional chain 讓這一行與它一致.
|
|
220
|
-
- **`readConfigFile` 只把 SyntaxError 當成不可用**: `[1,2,3]` 與 `"hello"` 都能正常 parse, 於是 spread 產生一個以索引為鍵的物件並被當成設定回傳, 使用者看到的是設定悄悄沒有生效, 而沒有任何訊息指名那個檔案. 現以型別閘門把它們導向同一條 warn-and-default 路徑. 改走既有的 zod `validateConfig` 會更一致, 但那會改變「部分錯誤的設定」的行為 — 未知鍵今天是刻意放行的 — 屬於行為決策, 此處刻意不做.
|
|
221
|
-
- **`--scope` 是整個 codebase 唯一的 `process.exit(1)`**: 對照 9 個檔案裡 36 處 `process.exitCode` 指派, 其中 12 處就在同一個檔案. `process.exit` 會在待寫出的 stdout/stderr 未必已 flush 時終止行程, 在 Windows pipe 上是有記錄的截斷來源. 它存活的理由是結構性的而非疏忽: 該檢查位於 commander 的 `preAction` hook, 在那裡 return 並不會阻止 action 執行, 所以 exit 是唯一看得出效果的做法. 修正補上缺少的中止機制 — 一個 `CliAbortError` 標記, 在 `runCli` 統一攔截, 而 `runCli` 原本完全沒有 try/catch, 因此也一併獲得一個把逸出的例外轉成乾淨 exit code 的地方. 勝過在 12 個子指令 action 裡各重複一次檢查 (那是比被修的缺陷更糟的一致性問題, 而第 13 個會漏掉), 也勝過 commander 的 `program.error()` — 它內部呼叫的還是 `process.exit`.
|
|
222
|
-
|
|
223
|
-
### 其他
|
|
224
|
-
|
|
225
|
-
- **本版五項修正各自立為 issue change 並歸檔**: `duplicate-key-silent-requirement-loss`、`preserve-line-endings-on-rewrite`、`archive-error-path-robustness`、`dashboard-trust-boundary-gaps`、`dashboard-overview-blocking-glob`. 全部來自一次針對自身程式碼的稽核而非上游比對, 因此上游 OpenSpec 參考點仍停在 `e062b95` (1.12.0). 本版未產生新的 ADR: 五項修正都落在既有決策的框架之內, 沒有推翻或新立任何取捨.
|
|
226
|
-
|
|
227
|
-
## [0.19.0-beta.0] - 2026-09-07
|
|
228
|
-
|
|
229
|
-
**預發布版本**: 需以 `npm install @seanmars/tospec@beta` 明確指定才會安裝, `latest` 仍指向 0.18.0.
|
|
230
|
-
|
|
231
|
-
本版本是對上游 OpenSpec `a0ddb60..e062b95` (1.12.0) 的比對結果. 四項修正共用同一個形狀: **指令回報的結論比它實際檢查過的內容更強**. validate 回報「通過」, 卻不說它發現了什麼, 也從未問過這些 delta 合併得起來嗎; 合併判定「這是一個新 capability」, 而依據只是那個檔案讀不到; `tospec list` 要求使用者初始化一個已經初始化過的專案. 四者的徵兆相同: 輸出是肯定句, 而背後沒有支撐它的檢查 — 使用者因此沒有任何理由去懷疑它.
|
|
232
|
-
|
|
233
|
-
### 新增
|
|
234
|
-
|
|
235
|
-
- **validate 回報 archive 會拒絕的 delta**: 驗證只把 delta 拿來與自己比對, 並在 `MODIFIED` 時比對主規格的 scenario, 從未問過主規格能不能提供這條 delta 要動的目標. `MODIFIED` 指向一條主規格沒有的 requirement、`RENAMED` 的來源已不存在、`ADDED` 撞上既有名稱 — 三者都驗證乾淨, 然後在 archive 時被拒絕, 而那通常是實作已經完成、撰寫當下的脈絡也早已消散之後. 這條缺口與 `change-validation` 這個 capability 自己的宗旨字面相反: 它存在的理由就是「驗證通過的 change 就是 archive 會接受的 change」. 現改為執行 archive 會執行的那次合併, 並把結果丟棄. 不重述合併的前提而是直接跑它: 那些前提裡有幾條刻意把「目標不存在」讀成「已經同步過」而非失敗, 第二份規則的副本遲早漂移, 而漂移的徵兆正是 validate 與 archive 各說各話 — 恰好是這條檢查存在要防的那一件事. 嚴重度為 INFO, 任何模式下都不改變判定: 目標不存在同時也是「改動一條姊妹 change 尚未歸檔的 requirement」的樣子, 而那在今天是合法的; 缺的是資訊, 不是判定. 結構檢查已經駁回的 delta 不重複回報 — 合併的抱怨描述的是後果而非錯誤本身, 會把讀者指向錯誤的行.
|
|
51
|
+
- **spec 合併加上跨行程 advisory lock**: 並行 archive 不再讓主 spec 少掉整條 requirement. archive 自身不再重跑一次合併預檢.
|
|
52
|
+
- **archive 的回報更誠實**: `--yes` 覆寫閘門後的警告進入成功文件的 `status`; `--skip-specs` 指名丟掉了哪些 capability; merge 通知延後到 spec 真的寫入之後; `--json` 合併 spec 不再要求 `--yes`; 沒有 TTY 時拒絕開選單, 互動中放棄回 exit 130; 歸檔目錄碰撞檢查移到主 spec 寫入之前, ticket 檔名相撞時留在原地並以 `ticket_archive_collision` 回報.
|
|
53
|
+
- **長度下限改為寬度感知**: 全形字元計為兩個單位, 中文寫的 `Why` 不再因字數被判太短.
|
|
54
|
+
- **`tospec migrate`**: `openspec/project.md` 折進 `config.yaml` 的 `context:`; REMOVED / RENAMED 改走與 archive 相同的 delta parser, RENAMED 實際套用; 遷移的 change 不補 ticket stub.
|
|
55
|
+
- **`init` / `update`**: 選單為空或 `--tools none` 時明說沒有寫入任何東西; profile 收窄後兩者都清理未選取與退役的 skill 目錄; `update` 在版本未變時說 Refreshed; `init` 不再指向不存在的 `README.md`.
|
|
56
|
+
- **skill 文案**: `tospec-propose` 動筆前必須檢視程式碼; `tospec-issue` 必須對帳 Test Plan 與 Tasks; 各 skill 對 `tospec validate` 改為讀整份報告 (每個 ERROR 都要修, 每個 WARNING 都是工作), `tospec-update` 補上 validate 步驟; `tospec-archive` 補齊 status code 清單與 `--yes` 的語意; 十份 SKILL.md 的重複段落逐一移除, 每條規則只留一份.
|
|
57
|
+
- **dashboard**: artifact 展開對單層 glob 走 readdir 快速路徑; metadata 解析失敗的 change 標為 unreadable 而非「還沒開始」; 沒有 commit 的 repo 上 `/api/activity` 降級為 `{available: false}`; `--detach` 等 child 回報綁定成功才印 URL, 失敗以非零狀態碼結束, 10 秒逾時; `--port` 加上整數與範圍檢查; wildcard 綁定時印出可連線的位址; `--help` 如實說明它會寫入.
|
|
58
|
+
- **相依套件升級**: TypeScript 7.0.2 (tsgo)、vitest 5、`@inquirer/core` 12、zod 4.6、marked 18.0.12; `build.js` 改以套件根目錄組出 tsc 路徑.
|
|
236
59
|
|
|
237
60
|
### 修正
|
|
238
61
|
|
|
239
|
-
- **
|
|
240
|
-
|
|
241
|
-
-
|
|
242
|
-
-
|
|
243
|
-
-
|
|
62
|
+
- **UTF-8 BOM**: 十一個 markdown reader 繞過 `normalizeDocument`, BOM 讓第一行對每條 `^` 錨定規則都不存在 (任務、`## Purpose`、ticket frontmatter 一起消失); `config.json`、stub 判定、兩條 read-modify-write 路徑與 `show` 的 H1 回退同樣中招. 現在 tospec 讀寫的檔案一律視 BOM 為不存在且永不寫回.
|
|
63
|
+
- **行尾與 code fence**: 改寫檔案時保留原本的 CRLF, 空行收合不再動 fenced 區塊; 任務計數、任務編號檢查與 `--require-sync` 的 `Conclusion:` 解析全部認得 code fence, fence 內的範例不再被當成真任務或真結論, 兩個互相矛盾的結論視為無法解析.
|
|
64
|
+
- **`TASK_PATTERN` 與 GFM 對齊**: 接受 `+` 項目符號, `]` 後必須有空白, 與 GitHub 畫出來的 checkbox 一致.
|
|
65
|
+
- **delta 文法與合併**: 重複的 delta 區段與主 spec 重複 requirement 不再靜默丟棄; 同名 scenario 不能再繞過 MODIFIED 防漏; `## REMOVED Requirements` 的 bullet 形式在 `show --json` 解析得到; `## REMOVED Scenarios` 沒有搭配 MODIFIED 時不再是 no-op; MODIFIED 整塊取代時保留 `### Notes`; capability 資料夾內檔名寫錯的 delta 以 ERROR 指名; delta 的 `## Purpose` 適用主 spec 的長度與 placeholder 規則; structure guard 與 merge 共用同一支 requirement header regex; template 的說明區段改放 fenced 範例, 不再被 parser 讀成內容.
|
|
66
|
+
- **「讀不到」不再被當成「不存在」**: 合併、archive、sync-report、migrate 與 delta 走訪的裸 catch 只吞 ENOENT / ENOTDIR, EACCES 是 ERROR 而非「沒有 delta」或「新 capability」.
|
|
67
|
+
- **`.tospec.yaml`**: 解析失敗時 `validate` / `archive` 以取代 delta 要求的 ERROR 回報 (原本建議去設一個它讀不到的 `skip_specs`); `schema` 欄位改為選填, 沉默等同採用專案預設; `status` 終於讀取 `skip_specs`, 宣告無規格的 change 不再永遠未完成; `list --json` 回報實際解析到的 schema; 一個壞掉的 change 不再讓 `list` 整份消失, 改以 `change_unreadable` 原地帶著失敗.
|
|
68
|
+
- **`--json` 契約**: 扁平失敗信封至少 null 掉一個真實的 data key; 執行期失敗回報已解析的 `root`; 專案 `rules:` 與 context 被丟棄的警告進 `status[]` 而非只到 stderr; 通過的 `validate` 不再對 stderr 寫診斷; `--scope` 的中止改走 `CliAbortError` 而非 `process.exit`.
|
|
69
|
+
- **`config`**: `get` 對未知與未設定的 key 各給診斷, `--allow-unknown` 同時適用於 `get`; `unset` 修剪被清空的父節點; `set workflows` 在非 custom profile 下警告, `profile core` 不再把完整清單寫進設定檔; `set profile custom` 沒有清單時警告; 非物件的 JSON 走 warn-and-default.
|
|
70
|
+
- **`decision`**: `new --force` 就地改寫索引列而非追加, 檔案以 `wx` 排他建立; `--date` 以 `Date` 往返驗證; `new` / `list` 遵守 root 紀律, 不再留下半成品 root; topic 錯誤訊息不再自稱 Change name; `--reindex` 不把斷鏈列的 status 洗成 `unknown`; `list --status` 篩不到時不再叫你去建第一份 ADR.
|
|
71
|
+
- **`new change`**: 壞掉的 schema 在寫出任何東西前拒絕; `--description` 在無 ticket template 的 schema 下以 `ticket_unsupported` 回報而非丟棄; `archive` 為保留名稱; 同名 change 只保留一張 active ticket, archive 不再搬走舊的那張; ticket 的 `ref:` 指向 schema 自己的第一個 artifact.
|
|
72
|
+
- **`show`**: issue change 印 `task.md` 而非 ticket frontmatter; `--json` 帶 requirement 與 scenario 名稱; `--diff` 對 bullet 形式的 REMOVED 印出主 spec 將刪除的區塊; 同時是 change 與 spec 的名字只建議一次.
|
|
73
|
+
- **`status` / `instructions`**: `stub` artifact 不再形成沒有 nextSteps 的死路; 「All artifacts complete!」與 Progress 分母採同一定義; `instructions apply` 對 glob 後的 tasks 用同一個 resolver; 缺少的 template 說明來自哪一層覆寫; 章節提示的 template 路徑對套件內建 schema 改印絕對路徑.
|
|
74
|
+
- **rule 文件統一管理**: `rules/tospec/decision.md` 與 workflow 規則走同一套雜湊標記與衝突檢查, 手改後不再於下一次 `init` 靜默消失; 空的 workflow profile 不再讓 `update` 無法恢復 (安裝判準改為 rule 文件而非 skill 數量); 專案自有的 `.codex/rules/` 檔案不再被當成 tospec 殘留; `AI_TOOLS` 的相依環直接拋錯.
|
|
75
|
+
- **`validate` 的訊息對準問題**: 主 spec 缺 scenario 只回報一次; next-step hint 依失敗種類對應; `--type` 改用 commander choices; `<change> --type spec` 不再原樣印 ENOENT; `--all` 的 Details 對每個失敗各給一行 rerun 指令.
|
|
76
|
+
- **零星修正**: 巢狀子命令的 `--help` 提示指向真實命令; `--schema decision` 在三條路徑給同一個答案; `templates --schema` 對缺檔回 `exists: false`; 引數驗證失敗時 `root` 是否解析一致; `--port ''` 只認數字; DST 跳過的那個小時被 `isRealTimestamp` 接受; `.tospec.yaml` / `config.yaml` 解析錯誤訊息去掉懸空冒號, 且 `list` / `status` 對解析不了的 `config.yaml` 印出診斷; REMOVED / RENAMED 行 regex 不再二次方回溯; 大小寫只差的 capability 警告指向真實目錄; 兩個 SCREAMING_SNAKE 狀態碼改為 lower_snake; 非專案目錄四個指令統一回報 `no_tospec_root`; `init` 建立的空目錄留下錨點檔, clone 後佈局不再消失.
|
|
244
77
|
|
|
245
78
|
### 其他
|
|
246
79
|
|
|
247
|
-
- **上游 OpenSpec 參考點推進至 `e062b95` (1.12.0)
|
|
248
|
-
-
|
|
249
|
-
|
|
80
|
+
- **上游 OpenSpec 參考點推進至 `e062b95` (1.12.0)**, 比對判斷寫入 `latest-git-ref.md`; 其後各版與上游比對無關.
|
|
81
|
+
- **repo 不再追蹤 `tospec/`、`.agents/`、`.claude/`**: 本專案自身的 ADR、changes、tickets 與生成的 skill 目錄移出 git (296 個檔案), README 重寫; `TODO.md`、`report.md`、`update.md` 移除, `how-to-publish.md` 移入 `docs/`. 各 beta 版引用的 ADR 需從 git 歷史取回 (`git show c3cad0a:tospec/decisions/<file>`).
|
|
82
|
+
- **`CLAUDE.md` 更名為 `AGENTS.md`**, 供 Claude Code 與 Codex 共讀; 補記 batch 命令部分成功的 JSON 形狀, 以及任務編號刻意不檢查斷號的理由.
|
|
83
|
+
- **程式碼精簡**: 跨 `src/` 與 `test/` 的註解與重複分支收斂, 淨減約 3,300 行, 無行為變更.
|
|
84
|
+
- **測試**: 1581 passed / 1 skipped (109 個檔案). 各輪掃描的回歸測試以 `roundN-regressions.test.ts` 依輪次歸檔; spawn 真實 CLI 的測試依賴 `dist/`, 先 `pnpm build`.
|
|
85
|
+
- 各 beta 版本的逐項條目 (含被否決的替代方案與取捨) 保留在 git 歷史中: `git show a6a0ec7:CHANGELOG.md`.
|
|
250
86
|
|
|
251
87
|
## [0.18.0] - 2026-08-28
|
|
252
88
|
|
|
@@ -577,14 +413,7 @@ Dashboard 進化為可背景常駐、多專案並存的服務, 並補上 TDD 導
|
|
|
577
413
|
|
|
578
414
|
- 新增 `prepack` script 與 npm publish 的準備設定, 完備套件發行流程.
|
|
579
415
|
|
|
580
|
-
[0.
|
|
581
|
-
[0.19.0-beta.6]: https://github.com/seanmars/tospec/compare/v0.19.0-beta.5...v0.19.0-beta.6
|
|
582
|
-
[0.19.0-beta.5]: https://github.com/seanmars/tospec/compare/v0.19.0-beta.4...v0.19.0-beta.5
|
|
583
|
-
[0.19.0-beta.4]: https://github.com/seanmars/tospec/compare/v0.19.0-beta.3...v0.19.0-beta.4
|
|
584
|
-
[0.19.0-beta.3]: https://github.com/seanmars/tospec/compare/v0.19.0-beta.2...v0.19.0-beta.3
|
|
585
|
-
[0.19.0-beta.2]: https://github.com/seanmars/tospec/compare/v0.19.0-beta.1...v0.19.0-beta.2
|
|
586
|
-
[0.19.0-beta.1]: https://github.com/seanmars/tospec/compare/v0.19.0-beta.0...v0.19.0-beta.1
|
|
587
|
-
[0.19.0-beta.0]: https://github.com/seanmars/tospec/compare/v0.18.0...v0.19.0-beta.0
|
|
416
|
+
[0.20.0]: https://github.com/seanmars/tospec/compare/v0.18.0...v0.20.0
|
|
588
417
|
[0.18.0]: https://github.com/seanmars/tospec/compare/v0.17.0...v0.18.0
|
|
589
418
|
[0.17.0]: https://github.com/seanmars/tospec/compare/v0.16.0...v0.17.0
|
|
590
419
|
[0.16.0]: https://github.com/seanmars/tospec/compare/v0.15.1...v0.16.0
|