@seanmars/tospec 0.19.0-beta.8 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -310
- package/README.md +69 -82
- package/assets/dashboard/app.js +11 -0
- package/assets/dashboard/style.css +7 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +89 -106
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/config.d.ts +9 -17
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +293 -145
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/dashboard.d.ts +57 -99
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dashboard.js +190 -313
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/decision.d.ts +38 -27
- package/dist/commands/decision.d.ts.map +1 -1
- package/dist/commands/decision.js +296 -128
- package/dist/commands/decision.js.map +1 -1
- package/dist/commands/metrics.d.ts +28 -51
- package/dist/commands/metrics.d.ts.map +1 -1
- package/dist/commands/metrics.js +62 -93
- package/dist/commands/metrics.js.map +1 -1
- package/dist/commands/shared-output.d.ts +12 -27
- package/dist/commands/shared-output.d.ts.map +1 -1
- package/dist/commands/shared-output.js +22 -45
- package/dist/commands/shared-output.js.map +1 -1
- package/dist/commands/show.d.ts +4 -7
- package/dist/commands/show.d.ts.map +1 -1
- package/dist/commands/show.js +23 -11
- package/dist/commands/show.js.map +1 -1
- package/dist/commands/validate.d.ts +34 -58
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +225 -141
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/workflow/index.d.ts +1 -5
- package/dist/commands/workflow/index.d.ts.map +1 -1
- package/dist/commands/workflow/index.js +1 -5
- package/dist/commands/workflow/index.js.map +1 -1
- package/dist/commands/workflow/instructions.d.ts +14 -24
- package/dist/commands/workflow/instructions.d.ts.map +1 -1
- package/dist/commands/workflow/instructions.js +219 -128
- package/dist/commands/workflow/instructions.js.map +1 -1
- package/dist/commands/workflow/new-change.d.ts +2 -5
- package/dist/commands/workflow/new-change.d.ts.map +1 -1
- package/dist/commands/workflow/new-change.js +69 -32
- package/dist/commands/workflow/new-change.js.map +1 -1
- package/dist/commands/workflow/schemas.d.ts +1 -5
- package/dist/commands/workflow/schemas.d.ts.map +1 -1
- package/dist/commands/workflow/schemas.js +6 -17
- package/dist/commands/workflow/schemas.js.map +1 -1
- package/dist/commands/workflow/shared.d.ts +37 -42
- package/dist/commands/workflow/shared.d.ts.map +1 -1
- package/dist/commands/workflow/shared.js +23 -54
- package/dist/commands/workflow/shared.js.map +1 -1
- package/dist/commands/workflow/status.d.ts +7 -17
- package/dist/commands/workflow/status.d.ts.map +1 -1
- package/dist/commands/workflow/status.js +57 -72
- package/dist/commands/workflow/status.js.map +1 -1
- package/dist/commands/workflow/templates.d.ts +8 -8
- package/dist/commands/workflow/templates.d.ts.map +1 -1
- package/dist/commands/workflow/templates.js +32 -46
- package/dist/commands/workflow/templates.js.map +1 -1
- package/dist/core/archive.d.ts +22 -31
- package/dist/core/archive.d.ts.map +1 -1
- package/dist/core/archive.js +296 -283
- package/dist/core/archive.js.map +1 -1
- package/dist/core/artifact-graph/graph.d.ts +25 -42
- package/dist/core/artifact-graph/graph.d.ts.map +1 -1
- package/dist/core/artifact-graph/graph.js +45 -63
- package/dist/core/artifact-graph/graph.js.map +1 -1
- package/dist/core/artifact-graph/index.d.ts +1 -1
- package/dist/core/artifact-graph/index.d.ts.map +1 -1
- package/dist/core/artifact-graph/index.js +1 -1
- package/dist/core/artifact-graph/index.js.map +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +54 -120
- package/dist/core/artifact-graph/instruction-loader.d.ts.map +1 -1
- package/dist/core/artifact-graph/instruction-loader.js +129 -111
- package/dist/core/artifact-graph/instruction-loader.js.map +1 -1
- package/dist/core/artifact-graph/outputs.d.ts +9 -23
- package/dist/core/artifact-graph/outputs.d.ts.map +1 -1
- package/dist/core/artifact-graph/outputs.js +45 -38
- package/dist/core/artifact-graph/outputs.js.map +1 -1
- package/dist/core/artifact-graph/resolver.d.ts +36 -81
- package/dist/core/artifact-graph/resolver.d.ts.map +1 -1
- package/dist/core/artifact-graph/resolver.js +60 -101
- package/dist/core/artifact-graph/resolver.js.map +1 -1
- package/dist/core/artifact-graph/schema.d.ts +0 -6
- package/dist/core/artifact-graph/schema.d.ts.map +1 -1
- package/dist/core/artifact-graph/schema.js +7 -32
- package/dist/core/artifact-graph/schema.js.map +1 -1
- package/dist/core/artifact-graph/state.d.ts +1 -8
- package/dist/core/artifact-graph/state.d.ts.map +1 -1
- package/dist/core/artifact-graph/state.js +2 -17
- package/dist/core/artifact-graph/state.js.map +1 -1
- package/dist/core/artifact-graph/stub-detection.d.ts +6 -14
- package/dist/core/artifact-graph/stub-detection.d.ts.map +1 -1
- package/dist/core/artifact-graph/stub-detection.js +13 -16
- package/dist/core/artifact-graph/stub-detection.js.map +1 -1
- package/dist/core/artifact-graph/types.d.ts +4 -0
- package/dist/core/artifact-graph/types.d.ts.map +1 -1
- package/dist/core/artifact-graph/types.js +30 -10
- package/dist/core/artifact-graph/types.js.map +1 -1
- package/dist/core/available-tools.d.ts +3 -12
- package/dist/core/available-tools.d.ts.map +1 -1
- package/dist/core/available-tools.js +4 -13
- package/dist/core/available-tools.js.map +1 -1
- package/dist/core/change-metadata/schema.d.ts +1 -1
- package/dist/core/change-metadata/schema.d.ts.map +1 -1
- package/dist/core/change-metadata/schema.js +10 -7
- package/dist/core/change-metadata/schema.js.map +1 -1
- package/dist/core/change-presenter.d.ts +16 -27
- package/dist/core/change-presenter.d.ts.map +1 -1
- package/dist/core/change-presenter.js +53 -53
- package/dist/core/change-presenter.js.map +1 -1
- package/dist/core/change-status-policy.d.ts +4 -8
- package/dist/core/change-status-policy.d.ts.map +1 -1
- package/dist/core/change-status-policy.js +9 -17
- package/dist/core/change-status-policy.js.map +1 -1
- package/dist/core/codex-metrics.d.ts +25 -45
- package/dist/core/codex-metrics.d.ts.map +1 -1
- package/dist/core/codex-metrics.js +44 -88
- package/dist/core/codex-metrics.js.map +1 -1
- package/dist/core/codex-residue.d.ts +14 -15
- package/dist/core/codex-residue.d.ts.map +1 -1
- package/dist/core/codex-residue.js +18 -22
- package/dist/core/codex-residue.js.map +1 -1
- package/dist/core/command-generation/adapters/claude.d.ts +2 -9
- package/dist/core/command-generation/adapters/claude.d.ts.map +1 -1
- package/dist/core/command-generation/adapters/claude.js +2 -12
- package/dist/core/command-generation/adapters/claude.js.map +1 -1
- package/dist/core/command-generation/adapters/index.d.ts +1 -9
- package/dist/core/command-generation/adapters/index.d.ts.map +1 -1
- package/dist/core/command-generation/adapters/index.js +1 -9
- package/dist/core/command-generation/adapters/index.js.map +1 -1
- package/dist/core/command-generation/generator.d.ts +0 -17
- package/dist/core/command-generation/generator.d.ts.map +1 -1
- package/dist/core/command-generation/generator.js +0 -17
- package/dist/core/command-generation/generator.js.map +1 -1
- package/dist/core/command-generation/index.d.ts +2 -5
- package/dist/core/command-generation/index.d.ts.map +1 -1
- package/dist/core/command-generation/index.js +0 -9
- package/dist/core/command-generation/index.js.map +1 -1
- package/dist/core/command-generation/types.d.ts +10 -36
- package/dist/core/command-generation/types.d.ts.map +1 -1
- package/dist/core/command-generation/types.js +0 -6
- package/dist/core/command-generation/types.js.map +1 -1
- package/dist/core/command-generation/yaml.d.ts +3 -18
- package/dist/core/command-generation/yaml.d.ts.map +1 -1
- package/dist/core/command-generation/yaml.js +5 -23
- package/dist/core/command-generation/yaml.js.map +1 -1
- package/dist/core/config-prompts.d.ts +2 -4
- package/dist/core/config-prompts.d.ts.map +1 -1
- package/dist/core/config-prompts.js +2 -7
- package/dist/core/config-prompts.js.map +1 -1
- package/dist/core/config-schema.d.ts +7 -41
- package/dist/core/config-schema.d.ts.map +1 -1
- package/dist/core/config-schema.js +35 -74
- package/dist/core/config-schema.js.map +1 -1
- package/dist/core/config.d.ts +25 -49
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +22 -45
- package/dist/core/config.js.map +1 -1
- package/dist/core/dashboard-activity.d.ts +7 -9
- package/dist/core/dashboard-activity.d.ts.map +1 -1
- package/dist/core/dashboard-activity.js +26 -24
- package/dist/core/dashboard-activity.js.map +1 -1
- package/dist/core/dashboard-data.d.ts +35 -22
- package/dist/core/dashboard-data.d.ts.map +1 -1
- package/dist/core/dashboard-data.js +57 -72
- package/dist/core/dashboard-data.js.map +1 -1
- package/dist/core/global-config.d.ts +24 -53
- package/dist/core/global-config.d.ts.map +1 -1
- package/dist/core/global-config.js +38 -67
- package/dist/core/global-config.js.map +1 -1
- package/dist/core/init.d.ts +12 -28
- package/dist/core/init.d.ts.map +1 -1
- package/dist/core/init.js +90 -168
- package/dist/core/init.js.map +1 -1
- package/dist/core/list.d.ts.map +1 -1
- package/dist/core/list.js +80 -38
- package/dist/core/list.js.map +1 -1
- package/dist/core/local-server.d.ts +41 -83
- package/dist/core/local-server.d.ts.map +1 -1
- package/dist/core/local-server.js +53 -98
- package/dist/core/local-server.js.map +1 -1
- package/dist/core/markdown-render.d.ts +15 -23
- package/dist/core/markdown-render.d.ts.map +1 -1
- package/dist/core/markdown-render.js +25 -34
- package/dist/core/markdown-render.js.map +1 -1
- package/dist/core/migrate.d.ts +19 -16
- package/dist/core/migrate.d.ts.map +1 -1
- package/dist/core/migrate.js +151 -135
- package/dist/core/migrate.js.map +1 -1
- package/dist/core/parsers/change-parser.d.ts +7 -10
- package/dist/core/parsers/change-parser.d.ts.map +1 -1
- package/dist/core/parsers/change-parser.js +48 -56
- package/dist/core/parsers/change-parser.js.map +1 -1
- package/dist/core/parsers/markdown-parser.d.ts +8 -9
- package/dist/core/parsers/markdown-parser.d.ts.map +1 -1
- package/dist/core/parsers/markdown-parser.js +23 -30
- package/dist/core/parsers/markdown-parser.js.map +1 -1
- package/dist/core/parsers/requirement-blocks.d.ts +43 -15
- package/dist/core/parsers/requirement-blocks.d.ts.map +1 -1
- package/dist/core/parsers/requirement-blocks.js +142 -70
- package/dist/core/parsers/requirement-blocks.js.map +1 -1
- package/dist/core/parsers/requirement-text.d.ts +73 -79
- package/dist/core/parsers/requirement-text.d.ts.map +1 -1
- package/dist/core/parsers/requirement-text.js +137 -79
- package/dist/core/parsers/requirement-text.js.map +1 -1
- package/dist/core/parsers/spec-structure.d.ts.map +1 -1
- package/dist/core/parsers/spec-structure.js +10 -6
- package/dist/core/parsers/spec-structure.js.map +1 -1
- package/dist/core/profiles.d.ts +3 -10
- package/dist/core/profiles.d.ts.map +1 -1
- package/dist/core/profiles.js +5 -12
- package/dist/core/profiles.js.map +1 -1
- package/dist/core/project-config.d.ts +43 -44
- package/dist/core/project-config.d.ts.map +1 -1
- package/dist/core/project-config.js +107 -82
- package/dist/core/project-config.js.map +1 -1
- package/dist/core/project-layout.d.ts +9 -17
- package/dist/core/project-layout.d.ts.map +1 -1
- package/dist/core/project-layout.js +16 -26
- package/dist/core/project-layout.js.map +1 -1
- package/dist/core/root-selection.d.ts +8 -14
- package/dist/core/root-selection.d.ts.map +1 -1
- package/dist/core/root-selection.js +3 -6
- package/dist/core/root-selection.js.map +1 -1
- package/dist/core/rules.d.ts.map +1 -1
- package/dist/core/rules.js +2 -3
- package/dist/core/rules.js.map +1 -1
- package/dist/core/schema-names.d.ts +16 -0
- package/dist/core/schema-names.d.ts.map +1 -0
- package/dist/core/schema-names.js +16 -0
- package/dist/core/schema-names.js.map +1 -0
- package/dist/core/schemas/base.schema.d.ts.map +1 -1
- package/dist/core/schemas/base.schema.js +6 -12
- package/dist/core/schemas/base.schema.js.map +1 -1
- package/dist/core/schemas/change.schema.d.ts +8 -0
- package/dist/core/schemas/change.schema.d.ts.map +1 -1
- package/dist/core/schemas/change.schema.js +41 -10
- package/dist/core/schemas/change.schema.js.map +1 -1
- package/dist/core/shared/index.d.ts +2 -7
- package/dist/core/shared/index.d.ts.map +1 -1
- package/dist/core/shared/index.js +2 -7
- package/dist/core/shared/index.js.map +1 -1
- package/dist/core/shared/rules-generation.d.ts +5 -15
- package/dist/core/shared/rules-generation.d.ts.map +1 -1
- package/dist/core/shared/rules-generation.js +33 -37
- package/dist/core/shared/rules-generation.js.map +1 -1
- package/dist/core/shared/skill-generation.d.ts +28 -43
- package/dist/core/shared/skill-generation.d.ts.map +1 -1
- package/dist/core/shared/skill-generation.js +82 -51
- package/dist/core/shared/skill-generation.js.map +1 -1
- package/dist/core/shared/tool-detection.d.ts +35 -76
- package/dist/core/shared/tool-detection.d.ts.map +1 -1
- package/dist/core/shared/tool-detection.js +73 -93
- package/dist/core/shared/tool-detection.js.map +1 -1
- package/dist/core/skill-metrics.d.ts +36 -63
- package/dist/core/skill-metrics.d.ts.map +1 -1
- package/dist/core/skill-metrics.js +34 -73
- package/dist/core/skill-metrics.js.map +1 -1
- package/dist/core/spec-presenter.d.ts.map +1 -1
- package/dist/core/spec-presenter.js +5 -10
- package/dist/core/spec-presenter.js.map +1 -1
- package/dist/core/specs-apply.d.ts +16 -31
- package/dist/core/specs-apply.d.ts.map +1 -1
- package/dist/core/specs-apply.js +146 -195
- package/dist/core/specs-apply.js.map +1 -1
- package/dist/core/templates/fragments/interview.d.ts +2 -6
- package/dist/core/templates/fragments/interview.d.ts.map +1 -1
- package/dist/core/templates/fragments/interview.js +2 -6
- package/dist/core/templates/fragments/interview.js.map +1 -1
- package/dist/core/templates/fragments/next-step.d.ts +4 -8
- package/dist/core/templates/fragments/next-step.d.ts.map +1 -1
- package/dist/core/templates/fragments/next-step.js +4 -8
- package/dist/core/templates/fragments/next-step.js.map +1 -1
- package/dist/core/templates/fragments/validate.d.ts +13 -0
- package/dist/core/templates/fragments/validate.d.ts.map +1 -0
- package/dist/core/templates/fragments/validate.js +13 -0
- package/dist/core/templates/fragments/validate.js.map +1 -0
- package/dist/core/templates/fragments/verify.d.ts +9 -12
- package/dist/core/templates/fragments/verify.d.ts.map +1 -1
- package/dist/core/templates/fragments/verify.js +9 -12
- package/dist/core/templates/fragments/verify.js.map +1 -1
- package/dist/core/templates/index.d.ts +0 -6
- package/dist/core/templates/index.d.ts.map +1 -1
- package/dist/core/templates/index.js +0 -7
- package/dist/core/templates/index.js.map +1 -1
- package/dist/core/templates/skill-templates.d.ts +1 -5
- package/dist/core/templates/skill-templates.d.ts.map +1 -1
- package/dist/core/templates/skill-templates.js +0 -5
- package/dist/core/templates/skill-templates.js.map +1 -1
- package/dist/core/templates/types.d.ts +3 -7
- package/dist/core/templates/types.d.ts.map +1 -1
- package/dist/core/templates/types.js +0 -3
- package/dist/core/templates/types.js.map +1 -1
- package/dist/core/templates/workflows/apply.d.ts +3 -9
- package/dist/core/templates/workflows/apply.d.ts.map +1 -1
- package/dist/core/templates/workflows/apply.js +11 -13
- package/dist/core/templates/workflows/apply.js.map +1 -1
- package/dist/core/templates/workflows/archive.d.ts +0 -6
- package/dist/core/templates/workflows/archive.d.ts.map +1 -1
- package/dist/core/templates/workflows/archive.js +16 -5
- package/dist/core/templates/workflows/archive.js.map +1 -1
- package/dist/core/templates/workflows/decision.js +3 -3
- package/dist/core/templates/workflows/decision.js.map +1 -1
- package/dist/core/templates/workflows/explore.js +1 -1
- package/dist/core/templates/workflows/grill.d.ts.map +1 -1
- package/dist/core/templates/workflows/grill.js +0 -2
- package/dist/core/templates/workflows/grill.js.map +1 -1
- package/dist/core/templates/workflows/issue.d.ts +0 -6
- package/dist/core/templates/workflows/issue.d.ts.map +1 -1
- package/dist/core/templates/workflows/issue.js +3 -2
- package/dist/core/templates/workflows/issue.js.map +1 -1
- package/dist/core/templates/workflows/propose.d.ts +0 -6
- package/dist/core/templates/workflows/propose.d.ts.map +1 -1
- package/dist/core/templates/workflows/propose.js +2 -2
- package/dist/core/templates/workflows/propose.js.map +1 -1
- package/dist/core/templates/workflows/sync.d.ts +2 -8
- package/dist/core/templates/workflows/sync.d.ts.map +1 -1
- package/dist/core/templates/workflows/sync.js +4 -3
- package/dist/core/templates/workflows/sync.js.map +1 -1
- package/dist/core/templates/workflows/update.d.ts +0 -6
- package/dist/core/templates/workflows/update.d.ts.map +1 -1
- package/dist/core/templates/workflows/update.js +9 -2
- package/dist/core/templates/workflows/update.js.map +1 -1
- package/dist/core/update.d.ts +10 -36
- package/dist/core/update.d.ts.map +1 -1
- package/dist/core/update.js +59 -126
- package/dist/core/update.js.map +1 -1
- package/dist/core/user-state-migration.d.ts +13 -15
- package/dist/core/user-state-migration.d.ts.map +1 -1
- package/dist/core/user-state-migration.js +16 -20
- package/dist/core/user-state-migration.js.map +1 -1
- package/dist/core/validation/constants.d.ts +4 -10
- package/dist/core/validation/constants.d.ts.map +1 -1
- package/dist/core/validation/constants.js +25 -25
- package/dist/core/validation/constants.js.map +1 -1
- package/dist/core/validation/prose-length.d.ts +15 -0
- package/dist/core/validation/prose-length.d.ts.map +1 -0
- package/dist/core/validation/prose-length.js +29 -0
- package/dist/core/validation/prose-length.js.map +1 -0
- package/dist/core/validation/purpose-placeholder.d.ts +9 -16
- package/dist/core/validation/purpose-placeholder.d.ts.map +1 -1
- package/dist/core/validation/purpose-placeholder.js +30 -44
- package/dist/core/validation/purpose-placeholder.js.map +1 -1
- package/dist/core/validation/section-validator.d.ts +4 -4
- package/dist/core/validation/section-validator.d.ts.map +1 -1
- package/dist/core/validation/section-validator.js +30 -12
- package/dist/core/validation/section-validator.js.map +1 -1
- package/dist/core/validation/task-numbering.d.ts +6 -3
- package/dist/core/validation/task-numbering.d.ts.map +1 -1
- package/dist/core/validation/task-numbering.js +23 -11
- package/dist/core/validation/task-numbering.js.map +1 -1
- package/dist/core/validation/types.d.ts +18 -0
- package/dist/core/validation/types.d.ts.map +1 -1
- package/dist/core/validation/types.js +12 -1
- package/dist/core/validation/types.js.map +1 -1
- package/dist/core/validation/validator.d.ts +42 -68
- package/dist/core/validation/validator.d.ts.map +1 -1
- package/dist/core/validation/validator.js +468 -285
- package/dist/core/validation/validator.js.map +1 -1
- package/dist/prompts/searchable-multi-select.d.ts +3 -8
- package/dist/prompts/searchable-multi-select.d.ts.map +1 -1
- package/dist/prompts/searchable-multi-select.js +16 -39
- package/dist/prompts/searchable-multi-select.js.map +1 -1
- package/dist/utils/change-metadata.d.ts +11 -50
- package/dist/utils/change-metadata.d.ts.map +1 -1
- package/dist/utils/change-metadata.js +48 -67
- package/dist/utils/change-metadata.js.map +1 -1
- package/dist/utils/change-utils.d.ts +24 -76
- package/dist/utils/change-utils.d.ts.map +1 -1
- package/dist/utils/change-utils.js +95 -142
- package/dist/utils/change-utils.js.map +1 -1
- package/dist/utils/file-lock.d.ts +39 -0
- package/dist/utils/file-lock.d.ts.map +1 -0
- package/dist/utils/file-lock.js +149 -0
- package/dist/utils/file-lock.js.map +1 -0
- package/dist/utils/file-system.d.ts +12 -32
- package/dist/utils/file-system.d.ts.map +1 -1
- package/dist/utils/file-system.js +16 -40
- package/dist/utils/file-system.js.map +1 -1
- package/dist/utils/frontmatter.d.ts +7 -11
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +11 -11
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/interactive.d.ts +4 -9
- package/dist/utils/interactive.d.ts.map +1 -1
- package/dist/utils/interactive.js +2 -4
- package/dist/utils/interactive.js.map +1 -1
- package/dist/utils/item-discovery.d.ts +10 -20
- package/dist/utils/item-discovery.d.ts.map +1 -1
- package/dist/utils/item-discovery.js +31 -55
- package/dist/utils/item-discovery.js.map +1 -1
- package/dist/utils/link.d.ts +9 -18
- package/dist/utils/link.d.ts.map +1 -1
- package/dist/utils/link.js +9 -18
- package/dist/utils/link.js.map +1 -1
- package/dist/utils/requirement-diff.d.ts +13 -23
- package/dist/utils/requirement-diff.d.ts.map +1 -1
- package/dist/utils/requirement-diff.js +13 -23
- package/dist/utils/requirement-diff.js.map +1 -1
- package/dist/utils/spec-files.d.ts +7 -20
- package/dist/utils/spec-files.d.ts.map +1 -1
- package/dist/utils/spec-files.js +26 -51
- package/dist/utils/spec-files.js.map +1 -1
- package/dist/utils/task-progress.d.ts +11 -9
- package/dist/utils/task-progress.d.ts.map +1 -1
- package/dist/utils/task-progress.js +53 -32
- package/dist/utils/task-progress.js.map +1 -1
- package/dist/utils/timestamp.d.ts +5 -8
- package/dist/utils/timestamp.d.ts.map +1 -1
- package/dist/utils/timestamp.js +5 -8
- package/dist/utils/timestamp.js.map +1 -1
- package/package.json +2 -3
- package/schemas/issue/schema.yaml +8 -1
- package/schemas/issue/templates/spec.md +20 -3
- package/schemas/sdd/schema.yaml +20 -1
- package/schemas/sdd/templates/spec.md +20 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,325 +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
|
-
|
|
26
|
-
|
|
27
|
-
- **dashboard 的 `/api/render` 不再輸出未淨化的 HTML**: client 端以 `innerHTML` 指派回應內容, 而該路徑沒有 CSP, 因此 `tospec/` 底下任何檔案裡的一個 `<img onerror>` 都能在 dashboard 的 origin 內執行 — 也就落在守衛 `POST /api/task` 的那道 CSRF 邊界之內: 注入的 script 不是跨來源請求, 該防線所倚賴的 preflight 對它從不適用, 所以寫入權等同開放. 修正分三層: raw HTML 一律轉義而非採用允許清單 (spec 文件沒有使用 HTML 的理由, 而允許清單是一項持續的維護義務, 漏掉一個標籤就等於漏掉全部); URL scheme 改為解析而非樣式比對 (`java<tab>script:` 在瀏覽器裡會正規化, 在 regex 裡不會); 並讓每個回應都帶 `script-src 'self'`, 使前兩層萬一漏掉時仍有一道不依賴淨化正確性的防線. 決策記錄: `tospec/decisions/20260918_171106-dashboard-markdown-escapes-raw-html.md`.
|
|
28
|
-
|
|
29
|
-
### 變更
|
|
30
|
-
|
|
31
|
-
- **`tospec archive` 新增 artifact 完整性閘門 (breaking change)**: 一個只有 `specs/<cap>/spec.md` — 沒有 `proposal.md`、沒有 `design.md`、沒有 `tasks.md` — 的 change 可以通過 `validate --strict` 並乾淨歸檔. 三個元件各持有部分視野而沒有任何一方提出異議: artifact graph 知道 sdd schema 把這三份標為 `optional: false`, 而它只回報、不設閘; `validate` 檢查的是已存在檔案的內容, 一份從未被寫出的檔案沒有內容可以失敗, 所以驗證是空洞地通過; `archive` 的閘門看的是驗證結果、任務勾選與 sync report, 沒有一個會去問 graph. 而歸檔是不可逆點 — change 移入 `tospec/changes/archive/`, delta 併入 `tospec/specs/`, 這套工作流程存在的理由 (why 與 how) 就此從未被記錄. 現在 archive 透過 `status` 所用的同一組 `loadChangeContext` + `formatChangeStatus` 詢問 graph, 以 `archive_artifacts_incomplete` 設閘並在訊息中指名缺哪幾份. 閘門可被 `--yes` 覆寫, 與 `archive_tasks_incomplete` 一致: 缺少規劃文件是規劃義務而非正確性義務, 呼叫端可能有其理由, 要求的是這項省略被說出來而不是被假定. 改為硬性錯誤遭否決 — 從他處匯入的 change、中途才採用 schema 的 change, 其文件永遠不會存在, 而本輪發現的失效是沉默, 不是寬鬆. 決策記錄: `tospec/decisions/20260917_155623-archive-gates-on-artifact-completeness.md`.
|
|
32
|
-
|
|
33
|
-
- **`status --json` 以 `optional` 取代部分情況下的 `skipped` (breaking change)**: 對 `skipped` 做特例處理的讀取端需要調整. 症狀是「schema 宣告為選用」與「這個 change 宣告不做」兩件事共用同一個狀態值, 而選用的 artifact 因此對 `nextSteps` 完全隱形, 永遠不會出現在任何一個下一步裡 — 一份選用但仍然值得寫的文件, 呼叫端不會被告知它存在. 兩者現在是相異的狀態. 決策記錄: `tospec/decisions/20260918_171157-optional-and-skipped-are-distinct-statuses.md`.
|
|
34
|
-
|
|
35
|
-
- **ticket frontmatter 的 `type` 改說 change-type 詞彙, schema 另立 `schema:` 欄 (breaking change)**: ticket 寫 `type: <schema>`, 而同一個 change 的 `.tospec.yaml` 與 `list --json` 寫 `type: <changeType>` — 一個鍵、兩套詞彙, 分佈在同一個 change 的兩份檔案裡. 對 `issue` 而言兩個詞恰好相同所以完全不可見; dashboard 早已把這道分裂吸收成一條同時比對 `.badge-requirement, .badge-sdd` 的 CSS 規則, 也就是說症狀早就被觀察到, 只是被當成樣式問題處理掉了.
|
|
36
|
-
|
|
37
|
-
- **`validate --strict` 的判準收緊到與 archive 一致 (breaking change)**: 原本綠燈的 change 可能開始被拒絕. 兩個成因分別修正 — (a) `findArchiveBlockers` 的預演早已算出正確答案, 卻歸檔在 `INFO` 而 `--strict` 只計警告, 於是 `validate --strict` 放行了 archive 即將拒絕的 change; 提升為 `WARNING` 而非 `ERROR`, 因為修改兄弟 change 尚未歸檔的 requirement 是受支援的情境, 用 ERROR 會擋掉它, 而 INFO 會讓歸檔前的閘門全盲. 決策記錄: `tospec/decisions/20260916_154000-archive-dry-run-findings-are-warnings.md`. (b) 見下方「`validate --strict` 對整份都是未填寫 template 的 change 回報 valid」.
|
|
38
|
-
|
|
39
|
-
- **`tospec instructions` 交付 schema 宣告的驗收條件**: `schema.yaml` 宣告了 `requiredSections` 與 `minSectionLength`, `validate` 一直都在強制執行, 而 loader 讀進來之後把它們丟掉了 — human 模式印出固定的 `<!-- To be defined in schema validation rules -->`, `--json` 則整段省略. agent 要得知 sdd 的 `Why` 有 50 字元下限, 唯一的途徑是把寫好的 proposal 送出去被退回. 那個空白區塊是較糟的一半: 它主張「這份 artifact 沒有驗收條件」, 是比沉默更強也更錯的宣稱. 規則現在隨 `ArtifactInstructions.validation` 一起交付, 而 artifact 未宣告任何條件時整個區塊省略, 與 `<project_context>`、`<rules>`、`<unlocks>` 既有的行為一致. 決策記錄: `tospec/decisions/20260917_234821-instructions-carry-acceptance-criteria.md`.
|
|
40
|
-
|
|
41
|
-
- **`archive --skip-specs` 由靜默丟棄改為具名警告**: 完全相同的情況經由 `skip_specs: true` 是一個硬性 ERROR, 經由 `--skip-specs` 旗標卻是無聲歸檔. 根因是兩者是不同種類的東西而外觀像同一個功能: `skip_specs` 是一項宣稱, validator 會去查核它; `--skip-specs` 是一道指令, 合併端直接照做. 旗標現在會指名它丟掉了哪些 capability. 維持為警告而非比照標記改為 ERROR: 旗標是使用者當下明確的指令, 把它變成錯誤等於讓這個旗標沒有用途. 決策記錄: `tospec/decisions/20260918_171157-skip-specs-flag-warns-rather-than-blocks.md`.
|
|
42
|
-
|
|
43
|
-
- **`archive --json` 一律帶 `totals`, `migrate --json` 補上 human 模式的後續步驟**: 前者原本在沒有任何合併發生時整個省略該鍵, 讀取端因此要區分「沒有欄位」與「數值為零」兩種情況, 而它們的意思相同; 現在未合併時歸零輸出. 後者原本漏掉延後的 ticket stub 提示與「接著執行 `tospec init`」這一步, 兩者 human 模式都會印.
|
|
44
|
-
|
|
45
|
-
### 修正
|
|
46
|
-
|
|
47
|
-
- **空的 workflow profile 讓 `tospec update` 無法恢復**: 工具是否已安裝的判斷來自 `SKILL.md` 的檔案數量, 而 `removeUnselectedSkillDirs` 刪除的正是那些檔案 — 於是清空目錄的那一次執行, 同時銷毀了該工具曾被設定過的唯一證據, 下一次 `update` 回報「No configured tools found」, 只剩 `init` 一條路可回. 根因是 `configured` 同時在回答兩個問題; 現在它回答「這個工具有沒有 skill」供 init 選單使用, 另立的 `isToolInstalled` 回答「tospec 是否管理這個目錄」供 update / rules 使用, 判準是任何 profile 變更都不會移除的 workflow rule 文件. 以 `rules/tospec/` 目錄的存在為判準遭否決: 該目錄同時放著 legacy 的 init-only `decision.md`, 它的存在只證明 tospec 曾經碰過這個目錄一次, 不證明現在仍管理它. `update` 另在清空前提出警告, 指名該負責的 profile 與兩個可以反轉它的命令. 決策記錄: `tospec/decisions/20260916_152000-tool-install-marker-not-skill-count.md`.
|
|
48
|
-
|
|
49
|
-
- **`rules/tospec/decision.md` 被手動編輯後在下一次 `tospec init` 靜默消失**: 同一個目錄底下的兩份產生檔案走在兩套不同的機制上. `single-source-of-truth.md` 經 `planWorkflowRules`: 出貨模板包在一個 sha256 標記裡, 任何寫入前先讀取, 位元組一旦不符即以衝突拒絕, 而拒絕訊息本身就說明 `--force` 不會繞過它. `decision.md` 則經 `writeToolRules`, 且只有 `init` 會呼叫: 沒有標記, 所以無從分辨產生檔案與手改檔案; 沒有計畫, 所以沒有任何東西會回報它; 寫入是無條件的. 編輯它會在下一次 `tospec init` 遺失 — 靜默、狀態碼 0、預設路徑、不需要 `--force`. 而 `tospec rules` — 這個命令的全部職責就是刷新 rule 檔案 — 從不碰它也不列出它, 所以這道漂移連經由設計用途的命令都修不回來. 原始碼在自己的檔頭註解裡點名了這件事 (「Legacy decision rules retain their init-only writer」) 卻沒有把它當成缺陷. 現在只有一套機制: `MANAGED_RULES` 列出每一份 rule 文件, `planWorkflowRules` 對全部進行規劃、雜湊標記、衝突檢查與回報. 讓 `init` 改成「不存在才寫」遭否決 — 它止住了資料遺失, 卻讓該檔案對 `rules` 仍然不可見, 一份已漂移的副本依舊沒有任何命令會說出來. `unmarkedLegacyBodies` 認得舊 writer 產出的確切文字, 因此既有專案靜默升級, 不會為一份沒人動過的檔案撞上衝突. 決策記錄: `tospec/decisions/20260917_234821-every-rule-document-is-managed-alike.md`.
|
|
50
|
-
|
|
51
|
-
- **一個壞掉的 change 就讓 `tospec list` 整份消失**: 一個 `.tospec.yaml` 無法解析的 change 使 `list` 以狀態碼 1 結束、`changes: []`, 每一個健康的 change 都不見了, 而訊息指名了那個未知的 schema 卻從不指名是哪一個 change 宣告了它 — 修好它唯一需要的那項資訊, 正是被扣住的那一項. `status.ts` 以散文寫下了這條政策 (「One malformed change must not blank the sweep, so the entry carries the failure in place instead of aborting」), `validate --all` 也遵守它, `list` 是第三個批次命令, 也是唯一中止的那個. 現在逐一以自己的 try/catch 讀取, 攜帶指名該 change 的 `change_unreadable` 診斷, 並在完整信封仍然送達 stdout 的前提下以狀態碼 1 結束. 壞掉的 change 即使在 `--type` 過濾下也維持列出, 因為過濾讀的正是剛剛失敗的那份 metadata — 把它排除掉, 等於藏起使用者必須修好才能看到其餘內容的那一個. 決策記錄: `tospec/decisions/20260917_234821-batch-commands-degrade-per-item.md`.
|
|
52
|
-
|
|
53
|
-
- **拼錯的 REMOVED 標頭以乾淨的成功歸檔**: `buildUpdatedSpec` 用 `!options.silent` 包住它的非致命警告, 而 archive 設定 `silent: json` — 這道守衛精準地壓制了沒有 console 可讀的那些呼叫端, 留下 `removed: 0`、狀態碼 0、空的 `status[]`, 而該 requirement 仍留在主 spec 裡. 警告現在以帶碼的 notice 回傳並導入 `status[]`; 由同一個 helper 同時負責記錄與列印, 使任何呼叫點都無法只做一半. 比照 MODIFIED / RENAMED 把懸空的 REMOVED 改為致命遭否決: 重新套用一個已經同步過的移除是 no-op, 而早期同步這個模式正倚賴它, 所以修法是讓它可見, 不是讓它失敗. 決策記錄: `tospec/decisions/20260916_153000-spec-merge-notices-are-returned.md`.
|
|
54
|
-
|
|
55
|
-
- **同名的 scenario 讓 MODIFIED 的防漏檢查失效, 可以無聲刪掉一條 scenario**: 名稱是 `findMissingScenarios` 唯一的把手, 所以兩條共用同一個名稱的 scenario 對它而言可以互換 — 把其中一條重述兩次, 數量吻合, 另一條的內容則在歸檔時被刪除, 而全程驗證皆為綠燈. 重複名稱進入主 spec 的唯一途徑是從 delta 合併進來, 所以現在就在 delta 這一端拒絕它.
|
|
56
|
-
|
|
57
|
-
- **`validate --strict` 對「整份都是未填寫 template」的 change 回報 valid**: 三份原封不動的 template 依其構造必然滿足區段規則, 而 `minSectionLength` 把 HTML 註解算進長度 — proposal template 裡那句 71 字元的 `Why` 提示, 滿足了它正在提示的那個 50 字元下限, 而一個真正寫出來的十字回答反而收到警告. 註解不再計入長度, 且 `validate` 改為回報 `status` 早已算出的 stub 狀態, 兩者共用同一個述詞而非各自判斷. 決策記錄: `tospec/decisions/20260918_171157-validate-reads-the-stub-status-status-already-computes.md`.
|
|
58
|
-
|
|
59
|
-
- **`new change --schema <壞掉的 schema>` 成功, 產出永久不可用的 change**: `validateSchemaExists` 只檢查目錄存在, 從不載入檔案, 於是產出一個沒有 `type`、沒有 ticket、連自己的成功 payload 裡都沒有 `ticketPath` 的 change, 而沒有任何命令讀得了它. 更糟的是 `tospec schemas` 把壞掉的 schema 藏起來, `new change` 的錯誤訊息卻把它當成可用選項宣傳 — 兩個命令對同一份 schema 給出相反的答案. 現在在寫出任何東西之前先載入 schema, 壞掉的項目留在清單裡並攜帶其原因, 而每一份「Available schemas」清單都改由真正載入成功的那些組成.
|
|
60
|
-
|
|
61
|
-
- **ticket 帳本產生重複與錯配, archive 搬走的是舊的那一張**: 放棄一個 change 會留下它的 ticket (`rm -rf` 是唯一的途徑), 用同一個名稱重建會再寫一張, 而 archive 取 readdir 順序 — 也就是最舊的那張 — 因此把這次的執行歸檔到那個被放棄的嘗試的 ticket 底下, 並讓真正的那張永遠留在原地. 決策記錄: `tospec/decisions/20260918_171157-one-active-ticket-per-change-name.md`.
|
|
62
|
-
|
|
63
|
-
- **`show <issue-change>` 從不印出 `task.md`**: 主文件被寫死為 `proposal.md`, 而 issue schema 從不產生這份檔案, 於是 `show` 落到 ticket stub 並印出十一行 frontmatter, 取代了根因、修復計畫、測試計畫與任務清單. 主 artifact 現在從 schema 解析.
|
|
64
|
-
|
|
65
|
-
- **artifact 為 `stub` 時形成死路**: 檔案存在但內容仍是 template 的狀態下, `buildNextSteps` 只比對 `ready` 與全部完成, 所以這是唯一一個回報 `isComplete: false` 卻同時給出空 `nextSteps` 的狀態 — 沒有指示也沒有理由, 而這恰好是呼叫端無法自行推斷的情況, 因為目錄列表看起來是完整的. archive 接著把那些檔案稱為「missing required artifact(s)」, 把讀者送去磁碟上找檔案, 而它的 `fix` 指向的正是那個無話可說的 `status`. human 模式一直都指名了它 (`[!] design (stub: ...)`), 所以這單純是機器契約的缺口.
|
|
66
|
-
|
|
67
|
-
- **扁平 `--json` 失敗信封沒有任何 data key**: `status --change` 是第三個扁平 payload, 卻什麼都沒有 null 掉, 理由正是一輪之前的 ADR 已經否決過的那一個. 更關鍵的是該 ADR 所規定的兩項測試在空鍵集上都是空洞地通過 — 「失敗信封攜帶的每一個鍵在成功時都存在」對於零個鍵恆為真 — 所以規則現在改為直接斷言, 而 payload 會 null 掉 `changeName`. 決策記錄: `tospec/decisions/20260917_155623-flat-json-payloads-null-a-real-success-key.md`.
|
|
68
|
-
|
|
69
|
-
- **`decision` 指令線缺 root 紀律**: `decision new` 只建立 `tospec/decisions/`, 留下一個半成品 root, 而其他每個命令接著都把它解析為一個專案, 於是 `validate --all` 與 `status --all` 開始對一個從來不是專案的目錄回報乾淨的全面通過 — 這正是那兩個命令拒絕隱含 root 所要避免的空洞通過, 從側門重新進來了一次. `createChange` 一直都會補完 root 並附有說明理由的註解, 兩者現在共用 `completeRootStructure`. `decision list` 同樣會在任何地方都以狀態碼 0 回答「這裡沒有決策」, 現在比照其他批次命令拒絕隱含 root.
|
|
70
|
-
- **`decision new --force` 改寫檔案卻不更新 `index.md`**: 不追加第二列是對的, 什麼都不做則不是 — index.md 於是繼續宣傳前一份的標題與摘要, 而 `decision list` 從磁碟讀到的是新的. 現在以檔名比對就地改寫該列; 標題正是一次改寫最可能變動的東西.
|
|
71
|
-
- **`--date` 接受 `20261345_996199`**: 檢查只看形狀, 而這是唯一一個不由 `formatTimestamp` 產生的時間戳; 該錯誤值會成為檔名、帳本裡的人類可讀日期, 以及一個排序上壓過每一筆真實紀錄的鍵. 改以 `Date` 來回轉換而非逐欄位範圍檢查, 使月份長度與閏日都取自行事曆.
|
|
72
|
-
|
|
73
|
-
- **巢狀子命令的 `--help` 提示指向不存在或不相干的命令**: 提示用的是 `command.name()` — 葉節點的名稱. 對每一個 top-level 命令都正確, 因為兩者恰好重合; 對兩個巢狀命令則都錯: `new change` 指名了一個根本不是命令的字串, commander 因此印出 top-level help 並以狀態碼 0 結束, 於是這則建議看起來像是被回答了; 而 `decision new` 指名了建立 change 的那一組 — 一個真實存在、但回答另一個問題的命令. 現在由完整路徑組出.
|
|
74
|
-
|
|
75
|
-
- **一批對同一份資料給出兩個答案的較小修正**: `config set workflows` 在非 custom profile 下被接受、逐 id 驗證、儲存並由 `config list` 顯示, 然後被丟棄, 因為只有 `custom` 會去查詢它 — 在 `update` 之前沒有任何東西否定「它有生效」這個信念; 兩端現在都會警告, 而 `config list` 顯示時一併重述該但書. `config set --allow-unknown` 寫得進去的鍵, `config get` 讀不出來 — `list` 看得到、`unset` 移除得掉, 唯獨這個旗標存在的目的所服務的可腳本化讀取端說它無效; `get` 現在也接受該旗標, 只放寬已知鍵檢查, 絕不放寬原型安全檢查, 與 `set` 的切分方式相同. `POST /api/task` 會勾選 change 目錄下任何 `.md` 的 checkbox — 這不是路徑穿越 (root 限制成立), 但 `proposal.md` 與 `design.md` 正是這套工作流程存在所要保存的紀錄, 而該寫入在 UI 裡不留任何痕跡, 因為讀取端只回報 schema 追蹤的那份檔案; 兩端現在共用 `resolveTaskFiles`. 執行期失敗從 null-shape 回報 `root: null`, 即使解析其實已經成功 — `status --change nope` 會一邊列出該 root 底下可用的 change 一邊宣稱自己沒有 root, 呼叫端因此分不出「不是專案」與「專案沒問題, 只是 change 不存在」; 解析出的 root 現在記錄於 `resolveRootForCommand` 並僅由 `emitFailure` 填入, commander 層與 root 解析本身的失敗維持 `null`, 那對它們而言是準確的. `status --schema decision` 會規劃一個位於 `tospec/changes/<change>/decision.md` 的 artifact — `new change` 與 `instructions` 早已拒絕的孤兒 — 且其 `nextSteps` 要呼叫端去執行那個保證會拒絕的 `instructions`; 發出指示的那個命令, 正是沒有守衛的那一個. 另外四項是第三輪修正沒有觸及到的同類呼叫端: `list --type` 在解析 root 之前先驗證自己的參數, `init` / `update` / `rules` 在規則衝突時寫死 `root: null`, `instructions` 的「Valid artifacts」清單漏了 `apply`, 以及 `--schema decision` 的守衛坐在 change 解析之後因而在空專案裡從不觸發.
|
|
76
|
-
|
|
77
|
-
- **第一輪的其餘修正**: 未放置於既有 capability 的新能力缺少 `## Purpose` 時, 會以字面上的 TBD 靜默進入合併後的 spec (模板補上該區段, archive 回報該次替換); `new change --schema decision` 建立出沒有命令能抵達的 change, `--decisions` 接受對應不到任何 ADR 的檔名 (兩者改為預先拒絕, ADR 模板不再宣稱一個沒有東西會寫入的反向連結); `show --json` 扣住 requirement 與 scenario 名稱 — 正是 MODIFIED / REMOVED / RENAMED delta 必須逐字重現、而 archive 會為此硬性失敗的那些字串; `--yes` 不再把「rerun with --yes」當成修法回聲給使用者; 兩個 SCREAMING_SNAKE 狀態碼改為 lower_snake; 非專案目錄在 list / status / archive / validate 四處統一回報單一的 `no_tospec_root`; dashboard 的已歸檔計數不再把 change 與其 ticket 重複計入; `dashboard -d` 不再重複 `Error:` 前綴; 無 delta 時的訊息改以 `skip_specs` 為誠實的替代方案, 而非暗示去發明一條 requirement; workflow rule 檔名去掉 `sourc` 這個錯字, 遷移邏輯讀舊 slug、先寫再刪, 且遇到已編輯的副本時拒絕而非移除.
|
|
78
|
-
|
|
79
|
-
### 其他
|
|
80
|
-
|
|
81
|
-
- **四輪掃描的方法與結果**: 測試由 83 檔 / 1112 passed 成長至 88 檔 / 1279 passed (1 skipped 為既有的 `it.runIf(platform !== 'win32')`, 與本版改動無關). 新增的測試大多正是它們的缺席才讓這些缺陷通過的那些斷言 — 扁平失敗信封至少攜帶一個 data key、`--help` 提示指名一個真實存在的命令、被編輯過的 rule 文件能存活過每一個會寫入規則的命令、遷移結果通過 `tospec validate --all`. 第二輪與第三輪的修正合併於同一個 commit: 第二輪的 11 項修復當時尚未提交, 而第三輪的 15 項觸及其中 13 個相同檔案, 拆開會需要 hunk 層級的手術, 並產生一個連自己的測試都跑不過的中間 commit.
|
|
82
|
-
|
|
83
|
-
- **`src/core/markdown-render.ts` 的兩個裸 NUL 位元組改寫為 `\x00` 逸出序列**: URL 淨化用的字元類別 `[\x00- ]` 把 NUL 直接寫成裸的控制字元. 執行結果完全正確, 但 git 的二進位偵測因此把整個檔案判為 binary, 於是它的提交對這個檔案印出 `Bin 0 -> 4490 bytes` 而不是逐行 diff — 本版唯一一項安全性修正所在的檔案, 就這樣在沒有任何可讀 diff 的情況下通過了審查. 逸出序列在 regex 字元類別裡與裸位元組完全等價, 所以這是純粹的來源表述修正, 行為與測試數皆不變.
|
|
84
|
-
|
|
85
|
-
- 上游 OpenSpec 參考點仍停在 `e062b95` (1.12.0), 本版與上游比對無關.
|
|
86
|
-
|
|
87
|
-
## [0.19.0-beta.7] - 2026-09-16
|
|
88
|
-
|
|
89
|
-
**預發布版本**: 需以 `npm install @seanmars/tospec@beta` 明確指定才會安裝, `latest` 仍指向 0.18.0.
|
|
90
|
-
|
|
91
|
-
本版本分兩層. 第一層是十個各自診斷到根因的 issue change, 它們共用同一種形狀: **命令的回報與它實際做的事分岔** — 為沒做成的工作回報成功 (`--detach` 印出一個沒人在監聽的 URL、archive 合併不到任何 delta 仍以狀態碼 0 結束)、丟掉自己已經收集到的資訊 (`--yes --json` 覆寫閘門後的警告、`config get` 失敗時不說原因), 或是描述一個它沒有的行為 (`init` 指向不存在的 `README.md`、dashboard 自稱唯讀). 第二層是對這十個 change 的實作逐條複查 Fix Plan / Test Plan / Tasks 所發現的九項缺口 — 關鍵在於**它們全部通過了完整測試**. 這九項可歸成三類: 放寬判斷條件後沒有回頭問「現在還會 match 到什麼」(收緊會被既有測試擋下, 放寬只會讓新的輸入被接受, 而那些輸入照定義就沒有測試); 同一條分支上的兩個 change 互相推翻了對方註解所陳述的前提; 以及 Test Plan 的項目在轉寫成 Tasks 時流失, 而兩份清單之間沒有任何機制對帳. 下列各項已把複查的修正併入它所修正的條目, 而非另立一節.
|
|
92
|
-
|
|
93
|
-
### 變更
|
|
94
|
-
|
|
95
|
-
- **`config list --json` 的設定內容改放在 `config` 鍵之下 (breaking change)**: 輸出形狀由 `{ version, ...config }` 改為 `{ version, config: { ... } }`, 取值路徑由 `.profile` 變成 `.config.profile`. 症狀是執行 `tospec config set version 9 --allow-unknown` 之後, `config list --json` 的信封 `version` 會變成 `9`, 讀取端再也無從判斷這份文件的格式版本. 根因不在 `version` 這個欄位名稱, 而在**使用者資料與信封 metadata 共用同一個命名空間**: `GlobalConfigSchema` 是 `.passthrough()`, `--allow-unknown` 又刻意允許寫入這個 build 還不認識的任意 top-level key, 而展開發生在信封欄位之後, 所以使用者的 key 必定覆蓋信封; `root` 與 `status` 是同一個洞的另外兩個入口. 曾評估「展開後強制覆寫信封欄位」與「寫入時拒絕保留字」: 前者把資料遺失換了個方向 — 使用者確實寫進設定檔的 `version` 會從 `config list --json` 靜默消失, 而 `config get version` 仍看得到, 兩個命令對同一份設定給出不同答案; 後者修不了已經寫在設定檔裡的 key. 兩者還都需要隨信封演進手動維護一份保留字清單, 漏掉時沒有任何訊號, 只有巢狀化讓衝突在結構上不可能發生. 決策記錄: `tospec/decisions/20260916_121544-config-list-json-nests-under-config-key.md`.
|
|
96
|
-
|
|
97
|
-
- **delta spec 的 REMOVED / RENAMED 條目不再強制 `### Requirement:` 前綴**: 症狀是照出貨模板寫成 `- \`Old Name\`` 的 REMOVED 條目被判定為「no requirement entries parsed」, 而錯誤訊息建議改寫成 `### Requirement:` 區塊 — 一個模板從未示範過的形式. 根因是**被接受的語法只存在於一條 regex 裡**: 搜遍 `schemas/`、`assets/`、`src/core/templates/` 與 `.agents/`, `FROM:` 一次都沒出現, 唯一的說明是模板裡「RENAMED uses a FROM/TO pair」這句正確但不足以照做的註解. 因此修正不只放寬 parser, 而是三件事一起做: 在兩份 `templates/spec.md` 與兩份 `schema.yaml` 的 `specs` artifact instruction 裡放進可直接複製的範例 (後者才是 agent 經由 `tospec instructions specs` 實際收到的文字), 並在 RENAMED / REMOVED 區段存在卻解析出零筆時改印所需語法, 而非沿用指向無效修法的通用建議.
|
|
98
|
-
- **放寬的範圍隨後被重新界定**: 首次實作把 REMOVED 的 bullet 比對放寬成任意縮排、任意內容, 於是 `Migration:` 底下的巢狀續行與 `---` 分隔線各自都成了一筆 removed requirement — `---` 是 lazy quantifier 的典型陷阱, `(.+?)` 為了讓後續 pattern 成功而吃掉 token 中段, 解析出一個名為 `--` 的 requirement — 結果一份寫法完全合理的 REMOVED delta 反而拿到兩個假的 ERROR 而無法歸檔. 現限定為區塊頂層的單行 bullet: 不允許前導空白排除巢狀續行, 要求破折號後有分隔空白排除 `---` (這正是 Markdown 自己區分 list item 與 thematic break 的方式). RENAMED 維持寬鬆而不跟著收緊, 因為 `FROM:` / `TO:` 關鍵字本身就是錨點, 縮排與位置不參與判斷, 同樣的放寬在那裡不產生歧義. 決策記錄: `tospec/decisions/20260916_121552-removed-bullet-grammar-limited-to-top-level.md`.
|
|
99
|
-
|
|
100
|
-
- **`tospec archive` 的成功文件可帶 `status` 警告陣列**: `--yes` 覆寫任務閘門後, 該閘門的 `code`、`message` 與 `fix` 不再被丟棄, 而是以 `severity: "warning"` 進入成功文件的 `status`. 原本 `opts.yes` 分支在 JSON 模式下直接返回, 兩個閘門 (`archive_tasks_missing`、`archive_tasks_incomplete`) 攜帶的完整診斷就此消失. `!opts.json` 這個條件本身有正當理由 — 散文印在 stdout 會破壞 JSON 文件 — 但當初採取的做法是丟掉警告, 而不是把它導進文件自己的 `status` 陣列; 契約裡早就定義了 severity 欄位, 警告被丟棄唯一的原因是當時檯面上只有「印在 stdout」這一個選項. 狀態碼維持 0: archive 確實成功了, `--yes` 就是使用者這麼說的, 這是回報修正而非新增閘門.
|
|
101
|
-
- **失敗路徑同樣不再丟掉已收集的警告**: `--no-validate --yes --json` 會先觸發 `archive_confirmation_required` 警告, 若之後合併失敗, 失敗文件原本只帶錯誤. 而一次「跳過驗證之後才失敗」的執行, 正是讀者最需要知道那次覆寫的時候 — 它就是這次失敗之所以可達的原因.
|
|
102
|
-
|
|
103
|
-
- **`tospec dashboard` 的 `--help` 說明它會寫入**: 描述改為「task checkbox updates are the only writes, confined to this tospec root and blocked for archived or sync-certified changes」. 原描述與 `CLAUDE.md` 的架構段落都仍稱它唯讀, 而 `POST /api/task` 早已會把勾選寫回 change 的 tasks 檔; 模組自己的檔頭註解描述得完全正確, 沒跟上的是使用者在決定要不要開這個連接埠之前唯一會讀的那段文字.
|
|
104
|
-
|
|
105
|
-
- **`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」), 擋不下這一批.
|
|
106
|
-
|
|
107
|
-
- **`tospec init` 不再指向不存在的 `README.md`**: 收尾訊息中無條件輸出的 `Documentation: README.md` 已移除. 那行大概是為套件自身的 README 而寫, 但它在使用者的新專案裡呈現為一個專案相對路徑, 而 `init` 只寫入 `tospec/`、`.agents/` 與設定的工具目錄, 不會產生該檔. 相較於改指向套件首頁 URL, 直接移除較安全: `init` 本來就以具體的下一步作結, 而一個必須持續保持正確的連結, 就是一個還會再次過期的連結.
|
|
108
|
-
|
|
109
|
-
### 修正
|
|
110
|
-
|
|
111
|
-
- **`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 的本意正好相反.
|
|
112
|
-
|
|
113
|
-
- **`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` 或任何未來的啟動失敗仍會回報成功.
|
|
114
|
-
- **等待本身加上逾時**: 原先的握手只 race `LISTENING` 與 `exit` 兩個事件, 但一個被 spawn 的 process 有三種結局 — 成功、失敗, 以及**兩者皆非**. child 若綁定後卡住, parent 會永遠等下去, `--detach` 直接 hang 且沒有任何輸出. 現加上 10 秒逾時, 訊息說明的是「沒有觀測到 child 回報」而非宣稱啟動失敗 (child 仍在執行, 可能只是還沒起來), 並指向 `--list` / `--stop`. `child.unref()` 一併移進 `finally`: 在逾時這條路徑上 child 還活著, 未 unref 的 handle 會在錯誤都回報完之後繼續綁住 parent 的 event loop.
|
|
115
|
-
- **成功路徑補上真實 spawn 的測試**: 原本唯一的覆蓋是自己 emit `LISTENING http://...` 的 stub, 與實作對同一個字串雙向耦合 — 這種閉環只有在兩邊同時寫錯時才會失敗. 新的測試實際起一個 detached dashboard, 用 `--list` 確認看得到, 再以 `--stop` 收尾, 全程不提及那個 token.
|
|
116
|
-
|
|
117
|
-
- **`tospec dashboard --port` 完全沒有驗證**: `Number(options?.port ?? 5620)` 沒有 `Number.isInteger` 或範圍檢查, 所以 `notanumber` 變成 `NaN` 一路抵達 URL 字串; 連埠號遞增迴圈也擋不住, 因為 `used.has(NaN)` 恆為 false, 該值原封不動通過. 現以明確的整數與 `0..65535` 範圍檢查走標準錯誤路徑, `-p 0` 維持可用 — 它是把埠號選擇交給作業系統, `--list` 本來就正確回報實際埠號.
|
|
118
|
-
|
|
119
|
-
- **`tospec decision new --force` 覆寫檔案卻仍追加一列索引**: `appendIndexRow` 是無條件的, 當 `--force` 取代既有檔案時該檔的列已經存在, 於是索引多出一列重複. 既有的限制註解記錄了「append-only、不去重」這個決定, 但它設想的是「對同一個 topic 重跑 `new` 會多一列」, 沒有涵蓋 `--force` — 那裡的檔案並不是第二筆紀錄. 修正讓索引列以「這次寫入是新檔」為條件, 符合索引本身的語意: 一筆決策一列, 而不是一次呼叫一列.
|
|
120
|
-
- **存在性檢查不是原子的**: 檢查與寫入之間隔著 `loadTemplate` 與 `renderDecision`, 那是貨真價實的 I/O 寬度; 兩個 process 可以同時通過檢查、同時寫入, 而 `fs.writeFileSync` 預設的 `'w'` 旗標無條件截斷, 後者靜默覆蓋前者. 現改用 `{ flag: 'wx' }` 讓檔案系統來執行這道守衛, `EEXIST` 翻譯回既有的訊息, 使用者可見的行為不變; `--force` 維持 `'w'`. 任何 check-then-write 的方案都留有一個窗口, 無論把它縮到多小, 而檔案系統早就提供了這個原語.
|
|
121
|
-
|
|
122
|
-
- **capability 資料夾內檔名寫錯的 delta 驗證全綠卻被靜默丟棄**: `findSpecFiles` 只收集 basename 恰為 `spec.md` 的檔案, 而 validator 早已備有一整組針對「永遠不會被合併的 delta 檔」的守衛 (specs 根目錄的 `spec.md`、深度超過一層的路徑、點開頭的資料夾), 每一條都以同一句理由成立. 第四種失敗模式完全相同的情況 — 對的資料夾、錯的檔名 — 沒有守衛, 根因是**它在守衛迴圈看到之前就被 walker 過濾掉了**: 守衛只能裁決 walker 交給它的檔案. 合併端讀的同樣是寫死的 `spec.md`, 兩邊一致, 所以沒有任何地方回報衝突. 修正是新增一個回傳 `specs/` 下所有 `*.md` 的走訪器並在同一個迴圈裡以 ERROR 指名該檔, 而不是放寬合併路徑 — 合併只讀 `spec.md` 是版面配置的契約, 改動它會讓同一資料夾內的兩個檔案變得語意不明.
|
|
123
|
-
|
|
124
|
-
- **`tospec config get` 對未知的 key 與未設定的 key 都靜默失敗**: `getNestedValue` 對「這個 key 存在但沒有值」與「這個 key 不屬於 schema」都回傳 `undefined`, 單一的 `undefined` 分支於是把兩種不同的使用者錯誤壓成同一次無聲離開. 根因是 `get` 從不驗證 key 是否為真 — `set` 不會有這個問題, 因為它在寫入前會對照 schema 驗證. 現先驗證 key (沿用 `set` 的判準, 兩個命令對「什麼是有效的 key」保持一致), 未知的 key 與未設定的 key 各給一則 stderr 診斷. stdout 在兩種情況下都維持空白, 保住 `(raw, scriptable)` 的契約.
|
|
125
|
-
|
|
126
|
-
- **`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 而未帶、來源為空, 或根本不存在.
|
|
127
|
-
- **產生的 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`, 讓縮排不再取決於被序列化的內容.
|
|
128
|
-
- **遷移的 change 不補 ticket stub**: summary 會回報有多少個 change 需要補. ticket 檔名帶時間戳, 而其語意是「這項工作被提出的時間」, migrate 推導不出正確的值 — 遷移當下的時間、檔案 mtime、OpenSpec 封存目錄名只有日期的前綴, 全都是編造. 產生一份時間不可信的 ticket, 會把一個本來只是「缺少」的狀態變成「存在但內容錯誤」, 而後者更難發現. 另補上「遷移結果通過 `tospec validate --all`」的回歸測試, 把「缺 ticket 不影響驗證」這個前提釘住. 決策記錄: `tospec/decisions/20260916_121552-migrated-changes-defer-ticket-stubs.md`.
|
|
129
|
-
|
|
130
|
-
- **數個 `--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` 既有的做法一致.
|
|
131
|
-
|
|
132
|
-
- **`tospec decision new` 的 topic 錯誤訊息自稱「Change name」**: 重用 `validateChangeName` 來檢查文法是對的 — topic 與 change name 共用同一套 kebab-id 文法, 而 `change-utils.ts` 是該文法唯一的定義處 — 缺陷在於它的錯誤字串是為單一呼叫端撰寫的, 另一個呼叫端直接包裝而未翻譯. 現讓 `validateChangeName` 接受呼叫端提供的名詞 (預設 `'Change name'`), `decision new` 傳入 `'Topic'`. 在 `decision` 呼叫點改寫字串會留下兩份會各自漂移的訊息, 修在共用驗證器才能維持一套文法、一套訊息.
|
|
133
|
-
|
|
134
|
-
- **`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` 後已含完全相同的三條建議).
|
|
135
|
-
|
|
136
|
-
### 其他
|
|
137
|
-
|
|
138
|
-
- **`tospec dashboard --detach` 的兩個 pid record writer 刻意保留, 並補上等價性測試**: parent 與 detached child 都會寫同一份 pid record. 複查一度認定 parent 那次是純重複, 實際判定是兩者各自為某一種失敗模式下唯一存在的紀錄 — child 那次是每個 dashboard (含前景執行) 為自己寫的, 也是 parent 在握手中途被砍或逾時放棄時僅存的一份; parent 那次則保證呼叫回傳時紀錄已經存在, 因為 child 是**送出 `LISTENING` 之後**才寫自己的, 少了它, 緊接著的第二次 `--detach` 會找不到東西可擋, 而綁定後卡住的 child 會變成追蹤不到的 orphan. 兩者寫入的位元組相同, docstring 已據實說明順序與理由; 新測試從子目錄啟動, 讓兩邊各自推導 `projectRoot` 再斷言等價, 一旦哪天分岔就會失敗.
|
|
139
|
-
|
|
140
|
-
- **本版第二層複查的基準與結果**: 十個 change 的 task.md 逐條對照實際程式碼, 加上 `pnpm build && pnpm test` 全綠 — 九項發現全部是「測試通過但仍然錯」的情況. 其中兩項有可獨立執行的紅燈訊號 (YAML block scalar 的 parse 錯誤、REMOVED delta 的假 ERROR), 其餘是註解與實際行為的落差, 或是邊界情況本來就沒有斷言. 複查修正完成後為 83 個檔案 / 1112 passed, 較基準新增 17 個測試; 1 skipped 為既有的 `it.runIf(platform !== 'win32')`, 與本版改動無關.
|
|
141
|
-
|
|
142
|
-
- 上游 OpenSpec 參考點仍停在 `e062b95` (1.12.0), 本版與上游比對無關.
|
|
143
|
-
|
|
144
|
-
## [0.19.0-beta.6] - 2026-09-14
|
|
145
|
-
|
|
146
|
-
**預發布版本**: 需以 `npm install @seanmars/tospec@beta` 明確指定才會安裝, `latest` 仍指向 0.18.0.
|
|
147
|
-
|
|
148
|
-
本版本來自對 `feature/sync-1.12.0` 整條分支的逐行審查 (165 個檔案, 約 10.9k 行新增), 共回報 14 項發現: 12 項修正, 2 項查證後判定不需修改. 這些缺陷散落在七個模組, 卻只有兩種形狀. 其一是**條件寫成「當初想得到的那幾種寫法」, 而不是它所代表的概念** — wildcard 位址只列三種拼法、空字串被當成使用者提供的名稱、殘留偵測掃了比目標更大的目錄; 每一條單獨讀都成立, 遇到沒被列舉的那一個就靜默走錯分支. 其二是**重構只搬走主要路徑, 邊界上的既有保證留在原地** — 清理被關進「這次有寫入東西」的條件裡、讀取失敗從「照舊附加」變成「整段放棄」、診斷訊息跟著 verdict 分流時漏掉通過的那一半. 本版另推翻 0.19.0-beta.5 引入的 skill 連結交付: 該機制在單一平台上是對的, 在跨平台團隊裡卻使提交進 git 的內容取決於貢獻者的作業系統.
|
|
149
|
-
|
|
150
|
-
### 變更
|
|
151
|
-
|
|
152
|
-
- **相依工具的 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`.
|
|
153
|
-
- **舊版留下的連結不需要遷移步驟**: `copyDir` 會先清空目的地, 而 `fs.rm(recursive)` 對 junction 或 symlink 是解除連結而非遞迴刪除, 因此停在 0.19.0-beta.5 的專案下次執行 `tospec update` 即就地轉換為複製, canonical 目錄不受影響.
|
|
154
|
-
- `init` 的輸出不再區分 linked / copied 兩種結果, 改為單一行說明哪些目標拿到複製, 以及編輯 canonical skill 後需要重跑; `DeliveryKind` 匯出一併移除.
|
|
155
|
-
- 這推翻的是 `20260910_214500` 的交付機制一項, 該 ADR 其餘決定 (agents 列為一般可選取列、`requires` 取代 always-on、清空選單等同 `--tools none`、偵測不到時的回退) 全數不受影響; 舊 ADR 已標記為部分被取代.
|
|
156
|
-
|
|
157
|
-
- **`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.
|
|
158
|
-
|
|
159
|
-
### 修正
|
|
160
|
-
|
|
161
|
-
- **`--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 閘門擋下.
|
|
162
|
-
- **`tospec archive ""` 會對互動選單剛列出的目錄丟出名稱格式錯誤**: 判斷名稱來源的 `nameFromArgument` 比對的是 `changeName !== undefined`, 於是空字串被算成「使用者提供了名稱」, 而後續的 `!changeName` 仍然把它送進互動選單. kebab 格式守衛接著就對選單剛回傳的目錄執行 — 而那道守衛自己的註解寫明它只該作用於參數路徑, 因為選單列出的是磁碟上真實存在的目錄, 其中有些早於這條約束 (`tospec migrate` 逐字複製 OpenSpec 的目錄名, snake_case 在那裡是慣例). 這是 0.19.0-beta.3 修好「守衛跑在選單結果上」之後, 由同一道守衛的另一個入口重新製造出來的. 現改為 `typeof changeName === 'string' && changeName.length > 0`: 空字串是沒有提供名稱, 不是提供了一個空的名稱.
|
|
163
|
-
- **ticket 無法讀取時, 歸檔會靜默丟失 Related Decisions 區段**: `appendDecisionLinks` 為了偵測 ticket 自身的換行慣例而先讀檔 (這是 0.19.0-beta.3 修正 CRLF 混用時加上的), 但讀取失敗時直接 `return`. `appendFile` 從頭到尾不需要讀取權限, 所以這個改動把一條原本能運作的路徑 — ticket 可附加但不可讀回 — 變成 Related Decisions 永久遺失, 而歸檔仍回報成功. 換行慣例是外觀問題, 決策連結是資料; 現在讀取失敗改為回退到 `\n` 並照常附加.
|
|
164
|
-
- **`--tools none` 不再清理退役的 skill 目錄**: `removeLegacySkillDirs` 在重構中被移進 `if (tools.length > 0)` 條件內. 但清除 tospec 自己在舊版產生的目錄, 與這次執行是否寫入新內容無關 — 被關進條件後, `--tools none` (使用者用來停止 tospec 寫入 agent 內容的方式) 反而讓 `.agents/skills/tospec-verify` 這類退役 workflow 留在磁碟上, 繼續被每個讀取 `.agents/skills/` 的 agent 載入, 與使用者的意圖正好相反. 現移回條件之外.
|
|
165
|
-
- **清空選單或 `--tools none` 會印出成功橫幅卻不說明什麼都沒寫**: 成功訊息的每一個區塊都以「選取非空」為前提, 於是空選取的執行會印出 `tospec Setup Complete`、config 行, 以及一行叫使用者執行 `tospec-propose` 的提示 — 而那是一個並不存在於磁碟上的 skill. 接受空選取是 `20260910_214500` 的決定, 沒有問題; 把它回報成一次普通的成功才是問題, 尤其清空選單是很容易誤觸的操作. 現會明說沒有寫入任何 skill、command 或 rule, 且不再建議一個不存在的 skill.
|
|
166
|
-
- **相依關係提示印給了沒有選到該工具的人, 且印在無法影響選擇的時機**: `discloseToolDependencies` 迭代整張 `AI_TOOLS` 並由 `execute()` 無條件呼叫 (僅 `--tools none` 例外), 所以 `tospec init --tools agents` 會印出「Claude Code 會把 skill 連結進 .agents/skills/, 因此選它也會一併選取 Agents」— 對方既沒有安裝 Claude Code, 也沒有任何選單可被這句話影響. 該方法自己的註解已經寫出理由: 這段說明存在的意義是「在使用者回答之後才抵達的解釋無法影響答案」, 而該前提只在有提示可回答時成立. 現改由互動選單路徑在提出問題前呼叫.
|
|
167
|
-
- **專案自有的 `.codex/rules/` 檔案會被誤判為 tospec 殘留**: `hasAnyRuleFile` 計算 `.codex/rules/` 底下任何位置的任何檔案, 但它觸發的提示宣稱那些檔案是 tospec 的產物, 並指名 `.codex/rules/tospec/` 為可刪除的對象. 於是一個從未執行過舊版 tospec、只是自己在 `.codex/rules/team-style.md` 放了規範的專案, 每次 `init` 與 `update` 都會收到這則提示, 而它指名的路徑並不存在. `writeToolRules` 只寫入 `rules/tospec/` 之下, 掃描範圍現與之對齊 — 與 skill 那一半早已限定 `tospec-` 前綴的做法一致, 也才符合該模組自己在下一段就寫明的承諾: 目錄單純存在並不構成殘留.
|
|
168
|
-
- **通過的 `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 為空.
|
|
169
|
-
- **`resolveToolSelection` 遇到相依環會產出錯誤的順序而不是拒絕**: 守衛在重新走到「正在拜訪中」的節點時提早 `return`, 等於吸收掉環而不是回報它, 而且仍然產出一份清單 — 由於內層呼叫先 push, 那份清單會把相依者排在它的相依對象之前. 呼叫端正是依照回傳順序寫入目標, 為的就是讓 canonical 目錄先存在再從中複製, 因此這份靜默的結果會讓它從一個尚未寫入的來源去產生工具目錄. 沒有任何順序能滿足一個環, 所以錯的是資料表本身, 必須在編輯發生的地方就說出來: 現改為丟出例外. 目前出貨的 `AI_TOOLS` 無環, 另有一項測試斷言這件事, 因此這道守衛保護的是未來新增的列而非現存狀況.
|
|
170
|
-
|
|
171
|
-
### 其他
|
|
172
|
-
|
|
173
|
-
- **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` 均無需為此修改.
|
|
174
|
-
- **兩項回報經查證後判定不需修改**: 其一是 `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 目錄零殘留.
|
|
175
|
-
- **審查修正立為 issue change 追蹤**: `tospec/changes/code-review-fixes/`, 14 項發現逐一記錄處理方式, 含上述兩項「查證後不修改」的理由. `CLAUDE.md` 的 generated-files 段落同步更新, 說明工具目錄是複製而非連結; 其中提到的三個手寫 skill 目錄 (`grill-me` / `grill-with-docs` / `grilling`) 已在 0.19.0-beta.5 退役, 該段敘述一併更正.
|
|
176
|
-
- 上游 OpenSpec 參考點仍停在 `e062b95` (1.12.0), 本版與上游比對無關.
|
|
177
|
-
|
|
178
|
-
## [0.19.0-beta.5] - 2026-09-11
|
|
179
|
-
|
|
180
|
-
**預發布版本**: 需以 `npm install @seanmars/tospec@beta` 明確指定才會安裝, `latest` 仍指向 0.18.0.
|
|
181
|
-
|
|
182
|
-
本版本是一次例行的相依套件更新: 依 npm-check-updates 回報的六個過時套件逐一升級, 三個 patch/minor 沒有引出任何相容性問題, 兩個 major 版本 (vitest、@inquirer/core) 也只是換了版號, 唯獨 TypeScript 5.9.3 直接跳到 7.0.2 這一步, 暴露出套件本身重構帶來的相容性落差. 六個套件刻意逐一升級並各自跑過完整 build 與 test, 而非一次套用全部版本再統一驗證, 使得唯一出狀況的那一個能被準確歸因, 不必事後在六個同時變動的套件裡回溯.
|
|
183
|
-
|
|
184
|
-
### 變更
|
|
185
|
-
|
|
186
|
-
- **六個過時相依套件依 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 個檔案) 不變, 再進入下一個.
|
|
187
|
-
- `@inquirer/core` 升級前, 直接相依其實已經與傳遞相依不同步: `@inquirer/prompts` 8.7.2 底下的 `@inquirer/input` 等子套件已要求 `@inquirer/core` `^12`, 專案自己宣告的卻仍是 `^11.2.1`, 使 `node_modules` 內同時存在兩份互不相容的 `@inquirer/core`. 升級後傳遞相依收斂回單一版本 (減少 4 個套件), 這次升級因此也是在修一個既有的相依漂移, 不只是取得新版號.
|
|
188
|
-
- `@types/node` 另補到 24.13.4, 純型別定義更新, 不影響執行期行為.
|
|
189
|
-
- `pnpm` 的供應鏈保護機制 (`minimumReleaseAge`) 認定 `zod@4.6.2` 發布時間過近, 自動在 `pnpm-workspace.yaml` 加入 `minimumReleaseAgeExclude` 例外項才放行安裝, 屬於 pnpm 自身的預期行為.
|
|
190
|
-
|
|
191
|
-
### 修正
|
|
192
|
-
|
|
193
|
-
- **`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/**/*` 的型別檢查結果不變, 沒有任何原始碼因這次升級而修改.
|
|
194
|
-
|
|
195
|
-
### 其他
|
|
196
|
-
|
|
197
|
-
- **本版未立為 sdd change, 也未新增 ADR**: 六個套件的版號調整與一處相依套件重構後的路徑解析修正, 屬於例行維護, 沒有推翻或新立任何架構取捨. 驗證涵蓋 `pnpm build`、`pnpm test`、`pnpm skills`/`pnpm templates` 兩支產出腳本, 以及對本專案自身 `tospec/` 目錄執行 `tospec validate --all` 與 `--version`/`--help` 的 CLI 手動驗證. 上游 OpenSpec 參考點仍停在 `e062b95` (1.12.0), 本版與上游比對無關.
|
|
198
|
-
|
|
199
|
-
> **補記 (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 推翻.
|
|
200
|
-
|
|
201
|
-
## [0.19.0-beta.4] - 2026-09-10
|
|
202
|
-
|
|
203
|
-
**預發布版本**: 需以 `npm install @seanmars/tospec@beta` 明確指定才會安裝, `latest` 仍指向 0.18.0.
|
|
204
|
-
|
|
205
|
-
本版本只改一份檔案的正文: 隨套件發布、載入到 agent 的 workflow 規則模板. 0.19.0-beta.3 已經把它縮減為正確的兩條原則, 這次要處理的不是它要求什麼, 而是它怎麼被讀 — 一份以權威身分載入 agent 的文字, 若只寫命令不寫理由, 它能約束的就只有它字面涵蓋到的情境.
|
|
206
|
-
|
|
207
|
-
### 變更
|
|
208
|
-
|
|
209
|
-
- **workflow 規則模板改寫為結構化英文, 每條規則附上自己的失效機制**: 模板正文原本是兩條 bullet, 第一條英文、第二條中文, `IMPORTANT:` 夾在第一條的句子中間, 兩條都只給命令不給理由. 混語言與埋沒的強調只是表面症狀; 成因在於這份檔案的身分 — 它是被當成權威來源載入 agent 的那一份文字, 而沒有理由的命令在邊界情境無法自我防守: agent 讀得懂「除非必要」的字面意思, 卻推不出違反它會發生什麼, 於是「必要」的門檻實際上由當下的方便程度決定. 第一條還有一個更具體的問題: 「以程式碼為準」沒有指出方向, 而 tospec 自身就內建 `/tospec-sync` 這種把規格對齊程式碼的流程, 「文件與程式碼不一致」因此很容易被讀成「該修程式碼讓它符合文件」, 恰好是這條規則要禁止的那一半. 現改為兩個具名 section, 各自寫出規則、可執行的行為 (回報落差而非改程式碼去迎合文件; 斷言前先在程式碼中驗證) 與失效機制 (文件記錄的是撰寫當下的意圖, 會隨程式演進漂移; 歸檔目錄不隨程式更新), 並把優先序提到標題下方獨立一行, 明講它高於文件、註解、規劃產物與先前對話. 只把中文那條翻成英文的最小改法被否決: 那修掉了語言不一致, 卻原樣留下真正會失效的部分. 規則語意一字未變, `agent-workflow-rules` 規格要求的三件事 (程式碼優先、預設不讀兩個 archive 目錄、只保留原則而不複製各 skill 的程序) 全數保留, 因此本次沒有 delta spec. 既有產出檔的正文仍符合自身記錄的雜湊, 下次 `init` / `tospec rules` / `update` 會判定為正常升級而非衝突, 使用者無須手動處理.
|
|
210
|
-
|
|
211
|
-
### 其他
|
|
212
|
-
|
|
213
|
-
- **本版未立為 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 個檔案).
|
|
214
|
-
|
|
215
|
-
## [0.19.0-beta.3] - 2026-09-10
|
|
216
|
-
|
|
217
|
-
**預發布版本**: 需以 `npm install @seanmars/tospec@beta` 明確指定才會安裝, `latest` 仍指向 0.18.0.
|
|
218
|
-
|
|
219
|
-
本版本把 0.19.0-beta.2 出貨時明列的七項缺陷全數修掉, 並替換掉同一版才引入的 workflow 規則正文. 七項缺陷沒有共同的機制, 只有共同的發現方式: 它們是審查 `main...HEAD` 時逐行讀出來的, 測試套件一項都沒有覆蓋到. 但它們確實共有一種形狀: **守衛檢查的內容是對的, 位置是錯的** — 檢查跑在互動選單覆寫過的變數上、掃描跑在比目標更大的索引上、路徑推導對歸檔目錄回答了一個不存在的 change 名稱、opt-in 只抵達兩道防護的其中一道. 每一個守衛單獨看都成立, 放在邊界的錯誤一側就什麼都沒有保護到. 而 beta.2 之所以帶著它們出貨, 是因為當時只把它們記成一份 issue change 與一支 probe script: 明列已知缺陷不等於處理它們.
|
|
220
|
-
|
|
221
|
-
### 變更
|
|
222
|
-
|
|
223
|
-
- **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`.
|
|
224
|
-
|
|
225
|
-
### 修正
|
|
226
|
-
|
|
227
|
-
- **`tospec archive` 的名稱格式檢查跑在互動選單的結果上**: 該守衛自己的註解寫明它只該作用於參數路徑, 因為互動選單列出的是磁碟上真實存在的目錄, 其中有些早於 `KEBAB_ID_REGEX` 這條約束. 程式碼卻把檢查放在 `changeName = selectedChange` 之後, 對同一個變數執行 — 於是選單前一秒才列出來的 snake_case change, 參數與選單兩條路徑都歸不了檔. 這不是罕見形狀: `tospec migrate` 逐字複製 OpenSpec 的目錄名, 而 snake_case 在那裡是慣例. 修正改為在互動分支有機會覆寫名稱之前先記下來源. 把整段檢查上移也能修好, 但會讓下一位讀者只能從敘述順序反推意圖, 而註解已經把意圖寫下來了.
|
|
228
|
-
- **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`.
|
|
229
|
-
- **delta spec 裡重複的一般標題被當成重複的 delta 區段擋下**: `findDuplicateSections` 掃的是未經過濾的一般區段索引, 於是一份 delta spec 寫了兩個 `## Purpose` 就會以「無法判斷哪些 requirement 屬於哪一段」的訊息驗證失敗 — 對一個不擁有任何 requirement 的標題而言這句話是假的, 而它足以擋下一份 delta 內容完全合法的檔案歸檔. 這是 0.19.0-beta.1 修正重複區段靜默遺失時一併引入的迴歸: 新增的拒絕路徑正是沒有人會走的那一條, 所以過寬的掃描範圍出貨時無人察覺. 掃描現限縮為四個 delta 標題, 沿用 parser 本來就在消費的那組名稱; 改為過濾 `splitTopLevelSections` 被否決, 那是一個通用索引, 其他呼叫端依賴它的完整性.
|
|
230
|
-
- **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, 而它確實應該以唯讀方式呈現歸檔文件.
|
|
231
|
-
- **metrics 重新整理失敗時, 會用舊的擷取時間標示新的數字**: 重新整理把兩份快取與 `capturedAt` 各自從自己的 await 直接指派出去. 第二次讀取拋錯時, 第一份快取已經換上新值而時間戳還停在舊的, 之後每一個請求都送出標著過期擷取時間的較新數字 — 正是 `capturedAt` 在 0.19.0-beta.1 被引入時要消除的那種歧義, 只是這次由失敗路徑重新製造出來. 兩次讀取現在都先落在區域變數, 三個欄位一起提交. 讓它正確的不是 `Promise.all`, 而是一起提交.
|
|
232
|
-
- **`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`.
|
|
233
|
-
- **格式錯誤的 spec id 在 `/api/specs` 回 500 而非 400**: `handleChangeDetail` 為它的 decode 加了守衛並回 400; `handleSpec` 做的是同一個 decode 卻沒有守衛, 於是 `%zz` 拋出的 `URIError` 以 500 帶著原始錯誤字串浮上來. 兩者早已分岔, 而一則註解還宣稱它們「刻意完全相同」 — 註解描述的是撰寫當下的意圖, 沒有任何東西強制它繼續為真. 兩者現在共用一個 `decodePathSegment`. 只修 `handleSpec` 也能還原那個不變式, 但會讓同一則註解有機會第二次過時.
|
|
234
|
-
|
|
235
|
-
### 其他
|
|
236
|
-
|
|
237
|
-
- **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.
|
|
238
|
-
- **三份新 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 個檔案).
|
|
239
|
-
- **儲存庫根目錄整理**: `how-to-publish.md` 移入 `docs/`, 內容未改; 刪除 `report.md` (v0.11.0 對 `output/` 的 skill 稽核) 與 `update.md` (對 2026-08-14 某個 commit 的上游比對分析) 兩份已完成任務的工作筆記. 兩者沒有任何東西引用, 而一份釘在舊上游 ref 的分析比沒有分析更糟 — 它讀起來像是現況. 選擇刪除而非移進 `docs/`: 需要那些推理時 git 歷史仍然握有它們.
|
|
240
|
-
|
|
241
|
-
## [0.19.0-beta.2] - 2026-09-10
|
|
242
|
-
|
|
243
|
-
**預發布版本**: 需以 `npm install @seanmars/tospec@beta` 明確指定才會安裝, `latest` 仍指向 0.18.0.
|
|
244
|
-
|
|
245
|
-
本版本只做一件事: 讓 agent 的 workflow 規則從「init 寫一次就再也回不去的檔案」變成套件能持續更新的產出物. 原本的規則正文是一個寫死在 TypeScript 裡的字串, 只在 init 途中寫出一次 — 而 init 是為「還沒有 tospec 的專案」設計的, 於是套件之後改了規則, 既有專案沒有任何指令拿得到. 一旦要讓它可更新, 下一個問題立刻成立且無法迴避: 產出檔與被使用者改過的檔案在磁碟上長得一模一樣, 而更新規則的每一種天真做法, 不是每次升級都誤判為衝突, 就是每次升級都靜默輾過使用者的修改.
|
|
246
|
-
|
|
247
|
-
### 新增
|
|
248
|
-
|
|
249
|
-
- **共用 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`.
|
|
250
|
-
- **產出檔內嵌版本標記與內容雜湊, 用來分辨套件升級與使用者手改**: 規則一旦可以被更新, 覆寫就成了問題 — 「內容與當前 template 不同」同時是這兩件事的樣子, 以它為判準必然二選一地錯: 要嘛每次套件升級都被當成衝突擋下, 要嘛使用者的每一次修改都在下一次 `update` 靜默消失. 根本原因是判準問錯了對象: 該問的不是「它跟現在的 template 一不一樣」, 而是「它還是不是一份沒被動過的產出」. 現在每份產出在框架標記中記錄自身正文的 SHA-256, 只要正文仍與它自己記錄的雜湊相符, 就是未經手改的產出 — 無論它出自哪一版 template, 都能安全換上新版; 正文被改、標記外多出內容、標記缺失、重複或格式錯誤, 則一律視為衝突並原樣保留. 選擇檔內自證而非在外部保存一份「上次產出了什麼」的紀錄: 後者能讓產出維持純文字, 但代價是第二份狀態, 而當它與檔案本身失去同步時, 沒有任何東西能判斷哪一邊才是對的. 比對前先正規化行尾, 因此以 CRLF 檢出的檔案不會被誤判為手改.
|
|
251
|
-
- **規則衝突在任何專案寫入之前整次擋下**: 三個入口都先對所有目標建立唯讀計畫, 任一目標衝突就整次失敗, 此時 skills、commands、設定、legacy 清理與另一個 agent 的規則全部尚未被動過. 若改為「輪到哪個 agent 才檢查它自己」, 第二個 agent 的衝突會留下第一個 agent 已經更新的檔案, 使用者拿到的是一個沒有任何指令描述得出來的中間狀態, 而重跑會再走一次同樣的部分更新. init 為此把兩件既有的寫入動作 — legacy user state 遷移, 以及以實際寫入探測權限的 validate — 移到預檢之後: 「衝突時不寫入任何專案檔案」這句話, 若把探測性的寫入排除在外就不成立. `--force` 不略過衝突檢查, 也不提供強制覆寫旗標: force 目前的意思是「即使產出已是最新也重新產生一次」, 而不是「丟棄使用者的修改」, 讓一個旗標同時代表這兩件事, 會使它在最該謹慎的時候最危險. 解除衝突的方式是自行保留修改內容、刪除該規則檔後重跑.
|
|
252
|
-
|
|
253
|
-
### 其他
|
|
254
|
-
|
|
255
|
-
- **本版的變更以 `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), 本版與上游比對無關.
|
|
256
|
-
- **另記錄了七項本版尚未修正的缺陷**: `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` 旗標.
|
|
257
|
-
|
|
258
|
-
## [0.19.0-beta.1] - 2026-09-08
|
|
259
|
-
|
|
260
|
-
**預發布版本**: 需以 `npm install @seanmars/tospec@beta` 明確指定才會安裝, `latest` 仍指向 0.18.0.
|
|
261
|
-
|
|
262
|
-
本版本不跟進上游, 而是一次針對自身程式碼的稽核. 五組修正共用同一個形狀: **規則早已寫在別處, 只是從未被抵達** — 「改寫檔案時保留其原本的行尾」寫在 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.
|
|
263
26
|
|
|
264
27
|
### 新增
|
|
265
28
|
|
|
266
|
-
- **
|
|
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`.
|
|
267
41
|
|
|
268
42
|
### 安全
|
|
269
43
|
|
|
270
|
-
-
|
|
271
|
-
|
|
272
|
-
-
|
|
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.
|
|
273
48
|
|
|
274
49
|
### 變更
|
|
275
50
|
|
|
276
|
-
- **
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
-
|
|
284
|
-
- parser 改回傳 `Map<string, SectionBody[]>`, 鍵為小寫標題, 每次出現各自解析後再串接結果. 保持各次出現分離而非合併其行, 有兩個決定性理由: 合併會跨越作者寫下的邊界捏造結構 — 一個 RENAMED 區段的 `FROM:` 會與另一段的 `TO:` 配對, 發明出兩段都沒有宣告的 rename; 而 `SectionBody.offset` 是單一基準索引, 四個呼叫端以 `offset + i + 1` 推導行號, 串接不相鄰的文件切片沒有合法的基準, 第二段回報的行號會靜默錯誤 — 正是本次要移除的那類缺陷. 逐次解析讓每個純量 offset 維持正確, 四個呼叫端一行未動.
|
|
285
|
-
- 鍵改小寫同時修掉同一種遺失的第二種樣貌: `## added requirements` 原本是另一個物件鍵, 而不分大小寫的查詢從此永遠找不到它. 改用 Map 也讓 `## __proto__` 這個標題落進自有項目, 而不是 `Object.prototype` 上的存取器.
|
|
286
|
-
- 驗證端直接拒絕重複的 delta 區段而非合併它: 兩個相同標題是編輯意外, 合併等於替作者猜測他想怎麼分組. 兩半分工不同 — parser 保證即使繞過驗證也不會遺失 (`archive --no-validate` 是真實存在的旗標), 驗證器則在修改還便宜的時候告訴作者這份檔案壞了.
|
|
287
|
-
- 主規格端新增 `duplicate-requirement` 這個 kind 到 `findMainSpecStructureIssues`, 而非在 `nameToBlock` 迴圈裡拋錯: 該函式本來就在合併前走過目標、本來就回報兩種結構缺陷、也本來就會讓 `buildUpdatedSpec` 在任何寫入之前中止, 因此不需要新的中止機制, 並免費地經由 `applySpecRules` 抵達 `tospec validate`. 放在 Map 迴圈裡的守衛則會在 `findMainSpecStructureIssues` 已經宣告該檔案乾淨之後, 從更低層再開火 — 兩個守衛對同一個檔案各說各話.
|
|
288
|
-
- **改寫檔案時破壞行尾字元與 fenced 區塊內的空行**: CLAUDE.md 已明文寫下這三處違反的規則 — 「改寫檔案時保留其原本的行尾」— 因此這是對既有約定的執行落差, 而非新政策; 它引用的前例也不是假設: 一個 CRLF 的 tasks 檔曾讓 archive 對一個任務其實已完成的 change 回報 `archive_tasks_missing`.
|
|
289
|
-
- 空行收合跑過整份文件. 它存在的理由是 recompose 串接的四段各自帶著邊界換行, 接縫會累積空行; 但 `replace(/\n{3,}/g, '\n\n')` 分不出接縫與作者寫在程式碼範例裡的空行, 於是每一次 archive 都悄悄改動了規格裡的範例. 改為逐行處理並以檔案內既有的 `buildCodeFenceMask()` 遮罩 — 同檔另外兩個函式已經在用它 — 接縫依建構方式必然落在 fence 之外, 遮罩不會損失任何收合原本要做的事.
|
|
290
|
-
- CRLF 的 `spec.md` 被改寫成 LF, 在 Windows 上產生整檔 diff, 把實際只動了一條 requirement 的變更埋掉. 行尾是在管線**前端**被破壞而非寫入時: `extractRequirementsSection` 呼叫 `normalizeDocument`, `normalizeBlockRaw` 又逐塊正規化一次, `recompose` 以 `'\n'` 串接 — 等 `writeUpdatedSpec` 執行時已經沒有東西可保留. 解析前正規化是正確的而且保留 (底下每一條結構 regex 都假設 LF, 而違反該假設正是當初 checkbox 缺陷的成因); 缺陷在於那個約定被丟棄而不是被記錄下來. 現由 `buildUpdatedSpec` 在原始位元組上偵測並回傳, `writeUpdatedSpec` 還原.
|
|
291
|
-
- 採整檔旗標而非 `setTaskDone` 用的逐行保真: 後者改寫的是一份其餘完全未動的檔案中的一行, 每一行的行尾都有定義; 而合併後的規格含有原本不存在的行, 逐行對它們沒有定義, 檔案層級的約定才是唯一涵蓋得了輸出的答案. 因此混合行尾的輸入會統一輸出為 CRLF — 那確實改動了原本沒被碰的那一半, 而這是正確的取捨, 因為另一個選項是替新行憑空發明一個行尾.
|
|
292
|
-
- `appendDecisionLinks` 附加的是硬編 LF 的區塊. `appendFile` 從不讀取檔案, 於是 ticket 自身的約定從未被查詢, 一份 CRLF 的 ticket 會變成標題之上 CRLF、之下 LF. 現在改為先讀取. 鄰居 `retargetTicketRef` 原本就是安全的, 但那是出於建構方式而非出於留意 — 它的 pattern 排除行終止符, 所以 replace 只會在行內改寫 — 該推理現已寫成註解, 以免下一位讀者把它「簡化」進同一個陷阱.
|
|
293
|
-
- **六處錯誤路徑缺陷隱藏或半套用了真實的失敗**: 六個小缺陷都落在「已經出事之後才會執行」的路徑上, 而它們所在的程式碼在成功路徑上異常小心. 合併記錄是因為它們共用這個性質, 其中兩個還共用一個更具體的成因: 守衛被放在邊界的錯誤一側.
|
|
294
|
-
- **archive 的歸檔目錄碰撞檢查跑在主規格寫入之後**: 走到該檢查, 代表規格已合併而 change 仍在 `tospec/changes/` 裡, 於是重試會再套用同一批 delta, REMOVED/RENAMED 接著走它們的「已同步」分支, 計數不再描述現實. 這與同一個函式自己陳述的不變式字面相反 — 「寫入任何一份之前先驗證每一份重建的規格, 使晚期的驗證失敗真的讓所有目標維持不變」. 該檢查只需要一個時間戳與一個名稱, 沒有任何東西迫使它必須晚; 現移到寫入之前, 時間戳只計算一次並沿用於搬移, 使被檢查的目錄就是被建立的目錄. 觸發機率很低 (需要同一秒內對同名 change 執行兩次), 但後果是一次沒有 undo 的半套用合併.
|
|
295
|
-
- **`Change not found` 從它自己的 try 之內拋出**: 該分支是死碼, 暗示一個並不存在的區別; 而那個裸 catch 同時把真正的 IO 失敗改寫成「找不到」— 權限問題被回報成 change 不存在. 改為 `fs.stat(...).catch(...)` 並以既有的 `isMissingPathError` 收窄, 只有真的不存在的路徑才能回答「找不到」. 單用 `.catch(() => null)` 會保留吞噬, 收窄才是重點.
|
|
296
|
-
- **`<name>` 從未檢查形狀**: 帶分隔符的名稱會讓 `changeDir` 指到 `tospec/changes/` 之外, 歸檔目標落在非預期的位置. 這不是安全邊界 — 那是一個已經握有 shell 的人給的 CLI 參數 — 但 `KEBAB_ID_REGEX` 已經定義了合法形狀而這條路徑忽略它, 於是一個打字錯誤換來的是困惑的失敗而不是清楚的訊息. 只在參數路徑驗證: 互動式選單列的是真實目錄, 其中有些早於這條約束, 拒絕一個 CLI 剛剛才提供的選項會讓可歸檔的 change 無法歸檔.
|
|
297
|
-
- **`shared-output` 的 `asStatus` 以裸屬性存取讀 `.diagnostic`**: 於是 `throw null` 或裸的 `Promise.reject()` 會讓那個存取本身拋錯 — 在錯誤處理器之內, 以一個不相關的 TypeError 取代真正的成因, 並完全抑制 JSON 外殼, 而這個 CLI 的契約是每一次 `--json` 失敗都恰好送出一份 JSON. 同檔的 `asErrorMessage` 早就防了這個形狀, 一個 optional chain 讓這一行與它一致.
|
|
298
|
-
- **`readConfigFile` 只把 SyntaxError 當成不可用**: `[1,2,3]` 與 `"hello"` 都能正常 parse, 於是 spread 產生一個以索引為鍵的物件並被當成設定回傳, 使用者看到的是設定悄悄沒有生效, 而沒有任何訊息指名那個檔案. 現以型別閘門把它們導向同一條 warn-and-default 路徑. 改走既有的 zod `validateConfig` 會更一致, 但那會改變「部分錯誤的設定」的行為 — 未知鍵今天是刻意放行的 — 屬於行為決策, 此處刻意不做.
|
|
299
|
-
- **`--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`.
|
|
300
|
-
|
|
301
|
-
### 其他
|
|
302
|
-
|
|
303
|
-
- **本版五項修正各自立為 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: 五項修正都落在既有決策的框架之內, 沒有推翻或新立任何取捨.
|
|
304
|
-
|
|
305
|
-
## [0.19.0-beta.0] - 2026-09-07
|
|
306
|
-
|
|
307
|
-
**預發布版本**: 需以 `npm install @seanmars/tospec@beta` 明確指定才會安裝, `latest` 仍指向 0.18.0.
|
|
308
|
-
|
|
309
|
-
本版本是對上游 OpenSpec `a0ddb60..e062b95` (1.12.0) 的比對結果. 四項修正共用同一個形狀: **指令回報的結論比它實際檢查過的內容更強**. validate 回報「通過」, 卻不說它發現了什麼, 也從未問過這些 delta 合併得起來嗎; 合併判定「這是一個新 capability」, 而依據只是那個檔案讀不到; `tospec list` 要求使用者初始化一個已經初始化過的專案. 四者的徵兆相同: 輸出是肯定句, 而背後沒有支撐它的檢查 — 使用者因此沒有任何理由去懷疑它.
|
|
310
|
-
|
|
311
|
-
### 新增
|
|
312
|
-
|
|
313
|
-
- **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 路徑.
|
|
314
59
|
|
|
315
60
|
### 修正
|
|
316
61
|
|
|
317
|
-
- **
|
|
318
|
-
|
|
319
|
-
-
|
|
320
|
-
-
|
|
321
|
-
-
|
|
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 後佈局不再消失.
|
|
322
77
|
|
|
323
78
|
### 其他
|
|
324
79
|
|
|
325
|
-
- **上游 OpenSpec 參考點推進至 `e062b95` (1.12.0)
|
|
326
|
-
-
|
|
327
|
-
|
|
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`.
|
|
328
86
|
|
|
329
87
|
## [0.18.0] - 2026-08-28
|
|
330
88
|
|
|
@@ -655,15 +413,7 @@ Dashboard 進化為可背景常駐、多專案並存的服務, 並補上 TDD 導
|
|
|
655
413
|
|
|
656
414
|
- 新增 `prepack` script 與 npm publish 的準備設定, 完備套件發行流程.
|
|
657
415
|
|
|
658
|
-
[0.
|
|
659
|
-
[0.19.0-beta.7]: https://github.com/seanmars/tospec/compare/v0.19.0-beta.6...v0.19.0-beta.7
|
|
660
|
-
[0.19.0-beta.6]: https://github.com/seanmars/tospec/compare/v0.19.0-beta.5...v0.19.0-beta.6
|
|
661
|
-
[0.19.0-beta.5]: https://github.com/seanmars/tospec/compare/v0.19.0-beta.4...v0.19.0-beta.5
|
|
662
|
-
[0.19.0-beta.4]: https://github.com/seanmars/tospec/compare/v0.19.0-beta.3...v0.19.0-beta.4
|
|
663
|
-
[0.19.0-beta.3]: https://github.com/seanmars/tospec/compare/v0.19.0-beta.2...v0.19.0-beta.3
|
|
664
|
-
[0.19.0-beta.2]: https://github.com/seanmars/tospec/compare/v0.19.0-beta.1...v0.19.0-beta.2
|
|
665
|
-
[0.19.0-beta.1]: https://github.com/seanmars/tospec/compare/v0.19.0-beta.0...v0.19.0-beta.1
|
|
666
|
-
[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
|
|
667
417
|
[0.18.0]: https://github.com/seanmars/tospec/compare/v0.17.0...v0.18.0
|
|
668
418
|
[0.17.0]: https://github.com/seanmars/tospec/compare/v0.16.0...v0.17.0
|
|
669
419
|
[0.16.0]: https://github.com/seanmars/tospec/compare/v0.15.1...v0.16.0
|