@seanmars/tospec 0.23.0 → 0.26.0-beta
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 +281 -0
- package/README.md +27 -9
- package/assets/claude-mod/tospec-dashboard/.claude-plugin/plugin.json +7 -0
- package/assets/claude-mod/tospec-dashboard/hooks/hooks.json +1 -0
- package/assets/claude-mod/tospec-dashboard/hooks/register.ts +695 -0
- package/assets/claude-mod/tospec-dashboard/tests/register.test.ts +448 -0
- package/assets/claude-mod/tospec-dashboard/types/index.d.ts +8 -0
- package/assets/dashboard/app.js +218 -1180
- package/assets/dashboard/index.html +31 -22
- package/assets/dashboard/lib/charts.js +355 -0
- package/assets/dashboard/lib/derive.js +375 -0
- package/assets/dashboard/lib/dom.js +121 -0
- package/assets/dashboard/lib/routes.js +52 -0
- package/assets/dashboard/lib/store.js +326 -0
- package/assets/dashboard/style.css +3103 -664
- package/assets/dashboard/views/alerts.js +170 -0
- package/assets/dashboard/views/change.js +234 -0
- package/assets/dashboard/views/docs.js +214 -0
- package/assets/dashboard/views/home.js +357 -0
- package/assets/dashboard/views/project.js +168 -0
- package/assets/dashboard/views/session.js +270 -0
- package/assets/dashboard/views/sidebar.js +131 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +116 -102
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +213 -100
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/dashboard-relay.d.ts +11 -0
- package/dist/commands/dashboard-relay.d.ts.map +1 -0
- package/dist/commands/dashboard-relay.js +88 -0
- package/dist/commands/dashboard-relay.js.map +1 -0
- package/dist/commands/dashboard.d.ts +69 -100
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dashboard.js +420 -598
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/decision.d.ts +38 -1
- package/dist/commands/decision.d.ts.map +1 -1
- package/dist/commands/decision.js +303 -27
- package/dist/commands/decision.js.map +1 -1
- package/dist/commands/metrics.d.ts +3 -4
- package/dist/commands/metrics.d.ts.map +1 -1
- package/dist/commands/metrics.js +3 -5
- package/dist/commands/metrics.js.map +1 -1
- package/dist/commands/shared-output.d.ts +24 -0
- package/dist/commands/shared-output.d.ts.map +1 -1
- package/dist/commands/shared-output.js +21 -2
- package/dist/commands/shared-output.js.map +1 -1
- package/dist/commands/show.d.ts.map +1 -1
- package/dist/commands/show.js +12 -9
- package/dist/commands/show.js.map +1 -1
- package/dist/commands/validate.d.ts +19 -0
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +243 -64
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/workflow/instructions.d.ts +7 -1
- package/dist/commands/workflow/instructions.d.ts.map +1 -1
- package/dist/commands/workflow/instructions.js +299 -81
- package/dist/commands/workflow/instructions.js.map +1 -1
- package/dist/commands/workflow/new-change.d.ts.map +1 -1
- package/dist/commands/workflow/new-change.js +26 -17
- package/dist/commands/workflow/new-change.js.map +1 -1
- package/dist/commands/workflow/prompt-escape.d.ts +25 -0
- package/dist/commands/workflow/prompt-escape.d.ts.map +1 -0
- package/dist/commands/workflow/prompt-escape.js +62 -0
- package/dist/commands/workflow/prompt-escape.js.map +1 -0
- package/dist/commands/workflow/schemas.d.ts.map +1 -1
- package/dist/commands/workflow/schemas.js +34 -16
- package/dist/commands/workflow/schemas.js.map +1 -1
- package/dist/commands/workflow/shared.d.ts +46 -10
- package/dist/commands/workflow/shared.d.ts.map +1 -1
- package/dist/commands/workflow/shared.js +66 -30
- package/dist/commands/workflow/shared.js.map +1 -1
- package/dist/commands/workflow/status.d.ts +5 -2
- package/dist/commands/workflow/status.d.ts.map +1 -1
- package/dist/commands/workflow/status.js +62 -28
- package/dist/commands/workflow/status.js.map +1 -1
- package/dist/core/archive.d.ts +38 -0
- package/dist/core/archive.d.ts.map +1 -1
- package/dist/core/archive.js +492 -75
- package/dist/core/archive.js.map +1 -1
- package/dist/core/artifact-graph/index.d.ts +2 -2
- package/dist/core/artifact-graph/index.d.ts.map +1 -1
- package/dist/core/artifact-graph/index.js +2 -2
- package/dist/core/artifact-graph/index.js.map +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +8 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts.map +1 -1
- package/dist/core/artifact-graph/instruction-loader.js +58 -36
- package/dist/core/artifact-graph/instruction-loader.js.map +1 -1
- package/dist/core/artifact-graph/resolver.d.ts +39 -12
- package/dist/core/artifact-graph/resolver.d.ts.map +1 -1
- package/dist/core/artifact-graph/resolver.js +82 -36
- package/dist/core/artifact-graph/resolver.js.map +1 -1
- package/dist/core/artifact-graph/schema.d.ts +24 -0
- package/dist/core/artifact-graph/schema.d.ts.map +1 -1
- package/dist/core/artifact-graph/schema.js +32 -1
- package/dist/core/artifact-graph/schema.js.map +1 -1
- package/dist/core/artifact-graph/stub-detection.d.ts +10 -2
- package/dist/core/artifact-graph/stub-detection.d.ts.map +1 -1
- package/dist/core/artifact-graph/stub-detection.js +18 -11
- package/dist/core/artifact-graph/stub-detection.js.map +1 -1
- package/dist/core/artifact-graph/types.d.ts +2 -2
- package/dist/core/artifact-graph/types.d.ts.map +1 -1
- package/dist/core/artifact-graph/types.js +10 -1
- package/dist/core/artifact-graph/types.js.map +1 -1
- package/dist/core/change-metadata/schema.d.ts.map +1 -1
- package/dist/core/change-metadata/schema.js +18 -4
- package/dist/core/change-metadata/schema.js.map +1 -1
- package/dist/core/change-presenter.d.ts.map +1 -1
- package/dist/core/change-presenter.js +13 -8
- package/dist/core/change-presenter.js.map +1 -1
- package/dist/core/change-status-policy.d.ts.map +1 -1
- package/dist/core/change-status-policy.js +3 -1
- package/dist/core/change-status-policy.js.map +1 -1
- package/dist/core/codex-metrics.d.ts +1 -1
- package/dist/core/codex-metrics.js +1 -1
- package/dist/core/config-schema.d.ts +8 -0
- package/dist/core/config-schema.d.ts.map +1 -1
- package/dist/core/config-schema.js +10 -0
- package/dist/core/config-schema.js.map +1 -1
- package/dist/core/dashboard-activity.d.ts +4 -4
- package/dist/core/dashboard-activity.js +4 -4
- package/dist/core/dashboard-data.d.ts +11 -5
- package/dist/core/dashboard-data.d.ts.map +1 -1
- package/dist/core/dashboard-data.js +54 -28
- package/dist/core/dashboard-data.js.map +1 -1
- package/dist/core/dashboard-hub.d.ts +258 -0
- package/dist/core/dashboard-hub.d.ts.map +1 -0
- package/dist/core/dashboard-hub.js +655 -0
- package/dist/core/dashboard-hub.js.map +1 -0
- package/dist/core/dashboard-routes.d.ts +57 -0
- package/dist/core/dashboard-routes.d.ts.map +1 -0
- package/dist/core/dashboard-routes.js +398 -0
- package/dist/core/dashboard-routes.js.map +1 -0
- package/dist/core/global-config.d.ts +71 -2
- package/dist/core/global-config.d.ts.map +1 -1
- package/dist/core/global-config.js +149 -4
- package/dist/core/global-config.js.map +1 -1
- package/dist/core/id.d.ts +9 -0
- package/dist/core/id.d.ts.map +1 -1
- package/dist/core/id.js +37 -0
- package/dist/core/id.js.map +1 -1
- package/dist/core/init.d.ts +9 -2
- package/dist/core/init.d.ts.map +1 -1
- package/dist/core/init.js +119 -33
- package/dist/core/init.js.map +1 -1
- package/dist/core/list.d.ts.map +1 -1
- package/dist/core/list.js +69 -45
- package/dist/core/list.js.map +1 -1
- package/dist/core/local-server.d.ts +10 -3
- package/dist/core/local-server.d.ts.map +1 -1
- package/dist/core/local-server.js +25 -5
- package/dist/core/local-server.js.map +1 -1
- package/dist/core/markdown-render.d.ts.map +1 -1
- package/dist/core/markdown-render.js +7 -2
- package/dist/core/markdown-render.js.map +1 -1
- package/dist/core/migrate.d.ts +24 -2
- package/dist/core/migrate.d.ts.map +1 -1
- package/dist/core/migrate.js +335 -59
- package/dist/core/migrate.js.map +1 -1
- package/dist/core/parsers/change-parser.d.ts +20 -1
- package/dist/core/parsers/change-parser.d.ts.map +1 -1
- package/dist/core/parsers/change-parser.js +76 -73
- package/dist/core/parsers/change-parser.js.map +1 -1
- package/dist/core/parsers/markdown-parser.d.ts +24 -0
- package/dist/core/parsers/markdown-parser.d.ts.map +1 -1
- package/dist/core/parsers/markdown-parser.js +65 -10
- package/dist/core/parsers/markdown-parser.js.map +1 -1
- package/dist/core/parsers/requirement-blocks.d.ts +127 -6
- package/dist/core/parsers/requirement-blocks.d.ts.map +1 -1
- package/dist/core/parsers/requirement-blocks.js +349 -56
- package/dist/core/parsers/requirement-blocks.js.map +1 -1
- package/dist/core/parsers/requirement-text.d.ts +25 -1
- package/dist/core/parsers/requirement-text.d.ts.map +1 -1
- package/dist/core/parsers/requirement-text.js +62 -6
- 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 +36 -24
- package/dist/core/parsers/spec-structure.js.map +1 -1
- package/dist/core/project-config.d.ts +9 -2
- package/dist/core/project-config.d.ts.map +1 -1
- package/dist/core/project-config.js +30 -7
- package/dist/core/project-config.js.map +1 -1
- package/dist/core/project-layout.d.ts +26 -0
- package/dist/core/project-layout.d.ts.map +1 -1
- package/dist/core/project-layout.js +51 -1
- package/dist/core/project-layout.js.map +1 -1
- package/dist/core/schema-names.d.ts +17 -0
- package/dist/core/schema-names.d.ts.map +1 -1
- package/dist/core/schema-names.js +24 -0
- package/dist/core/schema-names.js.map +1 -1
- package/dist/core/shared/dashboard-mod.d.ts +24 -0
- package/dist/core/shared/dashboard-mod.d.ts.map +1 -0
- package/dist/core/shared/dashboard-mod.js +44 -0
- package/dist/core/shared/dashboard-mod.js.map +1 -0
- package/dist/core/shared/index.d.ts +2 -1
- package/dist/core/shared/index.d.ts.map +1 -1
- package/dist/core/shared/index.js +2 -1
- package/dist/core/shared/index.js.map +1 -1
- package/dist/core/shared/rules-generation.d.ts +12 -0
- package/dist/core/shared/rules-generation.d.ts.map +1 -1
- package/dist/core/shared/rules-generation.js +26 -2
- package/dist/core/shared/rules-generation.js.map +1 -1
- package/dist/core/shared/skill-generation.d.ts +7 -0
- package/dist/core/shared/skill-generation.d.ts.map +1 -1
- package/dist/core/shared/skill-generation.js +19 -0
- package/dist/core/shared/skill-generation.js.map +1 -1
- package/dist/core/shared/tool-detection.d.ts +10 -1
- package/dist/core/shared/tool-detection.d.ts.map +1 -1
- package/dist/core/shared/tool-detection.js +37 -6
- package/dist/core/shared/tool-detection.js.map +1 -1
- package/dist/core/spec-presenter.d.ts.map +1 -1
- package/dist/core/spec-presenter.js +23 -1
- package/dist/core/spec-presenter.js.map +1 -1
- package/dist/core/specs-apply.d.ts +98 -1
- package/dist/core/specs-apply.d.ts.map +1 -1
- package/dist/core/specs-apply.js +426 -63
- package/dist/core/specs-apply.js.map +1 -1
- package/dist/core/templates/fragments/change-diff.d.ts +37 -0
- package/dist/core/templates/fragments/change-diff.d.ts.map +1 -0
- package/dist/core/templates/fragments/change-diff.js +37 -0
- package/dist/core/templates/fragments/change-diff.js.map +1 -0
- package/dist/core/templates/fragments/sync-fail.d.ts +8 -0
- package/dist/core/templates/fragments/sync-fail.d.ts.map +1 -0
- package/dist/core/templates/fragments/sync-fail.js +8 -0
- package/dist/core/templates/fragments/sync-fail.js.map +1 -0
- package/dist/core/templates/fragments/validate.d.ts +14 -1
- package/dist/core/templates/fragments/validate.d.ts.map +1 -1
- package/dist/core/templates/fragments/validate.js +14 -1
- package/dist/core/templates/fragments/validate.js.map +1 -1
- package/dist/core/templates/fragments/verify.d.ts +2 -2
- package/dist/core/templates/fragments/verify.d.ts.map +1 -1
- package/dist/core/templates/fragments/verify.js +8 -5
- package/dist/core/templates/fragments/verify.js.map +1 -1
- package/dist/core/templates/workflows/apply.d.ts +1 -1
- package/dist/core/templates/workflows/apply.d.ts.map +1 -1
- package/dist/core/templates/workflows/apply.js +29 -10
- package/dist/core/templates/workflows/apply.js.map +1 -1
- package/dist/core/templates/workflows/archive.d.ts.map +1 -1
- package/dist/core/templates/workflows/archive.js +27 -9
- package/dist/core/templates/workflows/archive.js.map +1 -1
- package/dist/core/templates/workflows/decision.d.ts.map +1 -1
- package/dist/core/templates/workflows/decision.js +10 -4
- package/dist/core/templates/workflows/decision.js.map +1 -1
- package/dist/core/templates/workflows/explore.d.ts +2 -2
- package/dist/core/templates/workflows/explore.d.ts.map +1 -1
- package/dist/core/templates/workflows/explore.js +7 -3
- package/dist/core/templates/workflows/explore.js.map +1 -1
- package/dist/core/templates/workflows/grill.d.ts +2 -2
- package/dist/core/templates/workflows/grill.d.ts.map +1 -1
- package/dist/core/templates/workflows/grill.js +5 -4
- package/dist/core/templates/workflows/grill.js.map +1 -1
- package/dist/core/templates/workflows/issue.d.ts.map +1 -1
- package/dist/core/templates/workflows/issue.js +9 -3
- package/dist/core/templates/workflows/issue.js.map +1 -1
- package/dist/core/templates/workflows/propose.d.ts.map +1 -1
- package/dist/core/templates/workflows/propose.js +4 -1
- package/dist/core/templates/workflows/propose.js.map +1 -1
- package/dist/core/templates/workflows/sync.d.ts.map +1 -1
- package/dist/core/templates/workflows/sync.js +25 -11
- package/dist/core/templates/workflows/sync.js.map +1 -1
- package/dist/core/templates/workflows/update.d.ts.map +1 -1
- package/dist/core/templates/workflows/update.js +17 -10
- package/dist/core/templates/workflows/update.js.map +1 -1
- package/dist/core/update.d.ts +5 -0
- package/dist/core/update.d.ts.map +1 -1
- package/dist/core/update.js +69 -29
- package/dist/core/update.js.map +1 -1
- package/dist/core/validation/constants.d.ts +7 -1
- package/dist/core/validation/constants.d.ts.map +1 -1
- package/dist/core/validation/constants.js +16 -2
- package/dist/core/validation/constants.js.map +1 -1
- package/dist/core/validation/purpose-placeholder.d.ts.map +1 -1
- package/dist/core/validation/purpose-placeholder.js +35 -11
- package/dist/core/validation/purpose-placeholder.js.map +1 -1
- package/dist/core/validation/section-validator.d.ts +7 -0
- package/dist/core/validation/section-validator.d.ts.map +1 -1
- package/dist/core/validation/section-validator.js +22 -2
- package/dist/core/validation/section-validator.js.map +1 -1
- package/dist/core/validation/sister-changes.d.ts +93 -0
- package/dist/core/validation/sister-changes.d.ts.map +1 -0
- package/dist/core/validation/sister-changes.js +180 -0
- package/dist/core/validation/sister-changes.js.map +1 -0
- package/dist/core/validation/task-markers.d.ts +9 -0
- package/dist/core/validation/task-markers.d.ts.map +1 -0
- package/dist/core/validation/task-markers.js +71 -0
- package/dist/core/validation/task-markers.js.map +1 -0
- package/dist/core/validation/types.d.ts +58 -0
- package/dist/core/validation/types.d.ts.map +1 -1
- package/dist/core/validation/types.js +22 -0
- package/dist/core/validation/types.js.map +1 -1
- package/dist/core/validation/validator.d.ts +79 -2
- package/dist/core/validation/validator.d.ts.map +1 -1
- package/dist/core/validation/validator.js +565 -84
- package/dist/core/validation/validator.js.map +1 -1
- package/dist/utils/change-metadata.d.ts +62 -5
- package/dist/utils/change-metadata.d.ts.map +1 -1
- package/dist/utils/change-metadata.js +193 -31
- package/dist/utils/change-metadata.js.map +1 -1
- package/dist/utils/change-utils.d.ts +21 -0
- package/dist/utils/change-utils.d.ts.map +1 -1
- package/dist/utils/change-utils.js +168 -53
- package/dist/utils/change-utils.js.map +1 -1
- package/dist/utils/command-line.d.ts +11 -0
- package/dist/utils/command-line.d.ts.map +1 -0
- package/dist/utils/command-line.js +43 -0
- package/dist/utils/command-line.js.map +1 -0
- package/dist/utils/display-width.d.ts +10 -0
- package/dist/utils/display-width.d.ts.map +1 -0
- package/dist/utils/display-width.js +15 -0
- package/dist/utils/display-width.js.map +1 -0
- package/dist/utils/file-system.d.ts +16 -0
- package/dist/utils/file-system.d.ts.map +1 -1
- package/dist/utils/file-system.js +49 -2
- package/dist/utils/file-system.js.map +1 -1
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +11 -5
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/interactive.d.ts.map +1 -1
- package/dist/utils/interactive.js +3 -1
- package/dist/utils/interactive.js.map +1 -1
- package/dist/utils/item-discovery.d.ts +3 -2
- package/dist/utils/item-discovery.d.ts.map +1 -1
- package/dist/utils/item-discovery.js +34 -16
- package/dist/utils/item-discovery.js.map +1 -1
- package/dist/utils/link.d.ts +0 -1
- package/dist/utils/link.d.ts.map +1 -1
- package/dist/utils/link.js +0 -1
- package/dist/utils/link.js.map +1 -1
- package/dist/utils/match.d.ts.map +1 -1
- package/dist/utils/match.js +10 -1
- package/dist/utils/match.js.map +1 -1
- package/dist/utils/option-flags.d.ts +9 -0
- package/dist/utils/option-flags.d.ts.map +1 -0
- package/dist/utils/option-flags.js +12 -0
- package/dist/utils/option-flags.js.map +1 -0
- package/dist/utils/sentence.d.ts +9 -0
- package/dist/utils/sentence.d.ts.map +1 -0
- package/dist/utils/sentence.js +12 -0
- package/dist/utils/sentence.js.map +1 -0
- package/dist/utils/spec-files.d.ts +84 -1
- package/dist/utils/spec-files.d.ts.map +1 -1
- package/dist/utils/spec-files.js +192 -3
- package/dist/utils/spec-files.js.map +1 -1
- package/dist/utils/task-progress.d.ts.map +1 -1
- package/dist/utils/task-progress.js +32 -9
- package/dist/utils/task-progress.js.map +1 -1
- package/dist/utils/timestamp.d.ts +27 -0
- package/dist/utils/timestamp.d.ts.map +1 -1
- package/dist/utils/timestamp.js +37 -0
- package/dist/utils/timestamp.js.map +1 -1
- package/package.json +6 -2
- package/schemas/decision/templates/decision.md +2 -2
- package/schemas/issue/schema.yaml +12 -6
- package/schemas/issue/templates/task.md +1 -1
- package/schemas/sdd/schema.yaml +21 -10
- package/schemas/sdd/templates/proposal.md +1 -1
- package/assets/dashboard/chart.umd.js +0 -14
package/dist/core/archive.js
CHANGED
|
@@ -3,19 +3,24 @@ import path from 'path';
|
|
|
3
3
|
import { getTaskProgressForChange, formatTaskStatus } from '../utils/task-progress.js';
|
|
4
4
|
import { Validator } from './validation/validator.js';
|
|
5
5
|
import { issueSymbol } from './validation/types.js';
|
|
6
|
-
import { validateChangeArtifacts } from '../commands/validate.js';
|
|
6
|
+
import { changeRequiresDeltas, validateChangeArtifacts } from '../commands/validate.js';
|
|
7
7
|
import { isRootSelectionError, resolveRootForCommand, toRootOutput, } from './root-selection.js';
|
|
8
|
-
import { findSpecUpdates, buildUpdatedSpec, writeUpdatedSpec, retireSpec, } from './specs-apply.js';
|
|
9
|
-
import { loadChangeContext, formatChangeStatus } from './artifact-graph/index.js';
|
|
8
|
+
import { findSpecUpdates, MainSpecMergeError, buildUpdatedSpec, writeUpdatedSpec, retireSpec, checkTargetWithinProject, } from './specs-apply.js';
|
|
9
|
+
import { loadChangeContext, formatChangeStatus, findSchemaWarnings, resolveSchema, } from './artifact-graph/index.js';
|
|
10
10
|
import { buildCodeFenceMask, normalizeDocument, stripBom } from './parsers/requirement-text.js';
|
|
11
|
+
import { parseDeltaSpec } from './parsers/requirement-blocks.js';
|
|
12
|
+
import { findSharedRequirements, listDeltaTouches, listSisterChangeDeltas, } from './validation/sister-changes.js';
|
|
11
13
|
import { formatTimestamp } from '../utils/timestamp.js';
|
|
12
|
-
import { findSpecFiles } from '../utils/spec-files.js';
|
|
14
|
+
import { findMisnamedDeltaFiles, findRootDeltaFiles, findRootSpecDelta, findSpecFiles } from '../utils/spec-files.js';
|
|
13
15
|
import { FileSystemUtils, isMissingPathError } from '../utils/file-system.js';
|
|
14
16
|
import { isKebabId, KEBAB_ID_DESCRIPTION } from './id.js';
|
|
15
17
|
import { findTicketPaths, listActiveChangeDirs } from '../utils/item-discovery.js';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
+
import { findLayoutPathNotDirectory, LayoutPathNotDirectoryError } from './project-layout.js';
|
|
19
|
+
import { isReservedChangeName, RESERVED_CHANGE_NAME_REASON } from '../utils/change-utils.js';
|
|
20
|
+
import { METADATA_FILENAME, readChangeMetadata, resolveSchemaForChange } from '../utils/change-metadata.js';
|
|
21
|
+
import { emitSuccess, emitFailureStatus, formatLabeled, indentContinuation, } from '../commands/shared-output.js';
|
|
18
22
|
import { withFileLock } from '../utils/file-lock.js';
|
|
23
|
+
import { isInteractive } from '../utils/interactive.js';
|
|
19
24
|
/** Alias of `formatTimestamp`: agents must never assemble archive stamps by hand. */
|
|
20
25
|
export const formatArchiveStamp = formatTimestamp;
|
|
21
26
|
/** `Conclusion: PASS` / `Conclusion: FAIL`, with an optional `(reason)` note. */
|
|
@@ -82,6 +87,11 @@ function toArchiveDiagnostic(error) {
|
|
|
82
87
|
if (isRootSelectionError(error)) {
|
|
83
88
|
return error.diagnostic;
|
|
84
89
|
}
|
|
90
|
+
// Raised by the change listing, which every other command reports by its own
|
|
91
|
+
// code; folding it into archive_error would make archive the one exception.
|
|
92
|
+
if (error instanceof LayoutPathNotDirectoryError) {
|
|
93
|
+
return error.diagnostic;
|
|
94
|
+
}
|
|
85
95
|
return {
|
|
86
96
|
severity: 'error',
|
|
87
97
|
code: 'archive_error',
|
|
@@ -106,6 +116,52 @@ export class MoveSourceNotRemovedError extends Error {
|
|
|
106
116
|
this.name = 'MoveSourceNotRemovedError';
|
|
107
117
|
}
|
|
108
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* A failure after archive changed something on disk. The fix names the manual
|
|
121
|
+
* steps left, and says not to rerun: the merge would refuse the change's ADDED
|
|
122
|
+
* requirements as already present, and a moved change is no longer found.
|
|
123
|
+
*/
|
|
124
|
+
function partiallyAppliedError(error, state) {
|
|
125
|
+
const rel = (target) => FileSystemUtils.toPosixPath(path.relative(state.projectRoot, target));
|
|
126
|
+
const changeRel = rel(state.changeDir);
|
|
127
|
+
const archiveRel = rel(state.archivePath);
|
|
128
|
+
const unwritten = state.unwrittenCapabilities ?? [];
|
|
129
|
+
const done = [];
|
|
130
|
+
if (state.mergedCapabilities.length > 0) {
|
|
131
|
+
done.push(`merged the specs of ${state.mergedCapabilities.join(', ')} into tospec/specs/`);
|
|
132
|
+
}
|
|
133
|
+
if (state.changeMoved)
|
|
134
|
+
done.push(`moved the change to ${archiveRel}`);
|
|
135
|
+
const failed = unwritten.length > 0
|
|
136
|
+
? `writing the specs of ${unwritten.join(', ')}`
|
|
137
|
+
: state.changeMoved
|
|
138
|
+
? 'archiving the ticket in tospec/tickets/archive/'
|
|
139
|
+
: `moving the change to ${archiveRel}`;
|
|
140
|
+
const cause = error instanceof Error ? error.message : String(error);
|
|
141
|
+
// "Still in": the ticket move can fail after the file moved, while its
|
|
142
|
+
// change link is rewritten, and the step must not send the user to a file
|
|
143
|
+
// that is no longer there.
|
|
144
|
+
const ticketStep = `move the change's ticket, if it has one still in tospec/tickets/, to tospec/tickets/archive/ and point its change link at ${path.basename(state.archivePath)}`;
|
|
145
|
+
let fix;
|
|
146
|
+
if (unwritten.length > 0) {
|
|
147
|
+
fix =
|
|
148
|
+
`Do not rerun archive: the merge would refuse the requirements it already added. ` +
|
|
149
|
+
`Merge the change's deltas for ${unwritten.join(', ')} into tospec/specs/ by hand, move ${changeRel} to ${archiveRel}, then ${ticketStep}.`;
|
|
150
|
+
}
|
|
151
|
+
else if (state.changeMoved) {
|
|
152
|
+
fix = `Do not rerun archive: the change is already archived. Finish by hand: ${ticketStep}.`;
|
|
153
|
+
}
|
|
154
|
+
else if (error instanceof MoveSourceNotRemovedError) {
|
|
155
|
+
fix = `Do not rerun archive. ${archiveRel} is complete: delete what is left of ${changeRel}, then ${ticketStep}.`;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
const clearCopy = state.partialCopyLeft ? ` (delete the partial copy at ${archiveRel} first)` : '';
|
|
159
|
+
fix =
|
|
160
|
+
`Do not rerun archive: the merge would refuse the requirements it already added. ` +
|
|
161
|
+
`Move ${changeRel} to ${archiveRel} by hand${clearCopy}, then ${ticketStep}.`;
|
|
162
|
+
}
|
|
163
|
+
return new ArchiveBlockedError('archive_partially_applied', `Archive stopped partway. Done: ${done.join('; ')}. Failed while ${failed}: ${cause}`, fix);
|
|
164
|
+
}
|
|
109
165
|
/**
|
|
110
166
|
* On Windows fs.rename() often fails with EPERM when the directory is non-empty
|
|
111
167
|
* or another process has it open (IDE, watcher, antivirus), so EPERM/EXDEV fall
|
|
@@ -118,9 +174,23 @@ export async function moveDirectory(src, dest) {
|
|
|
118
174
|
catch (err) {
|
|
119
175
|
const code = err?.code;
|
|
120
176
|
if (code === 'EPERM' || code === 'EXDEV') {
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
await fs.
|
|
177
|
+
// Only a destination this copy created is rolled back: one already there
|
|
178
|
+
// (rename onto it fails with EPERM on Windows) holds someone else's files.
|
|
179
|
+
const destExisted = await fs.lstat(dest).then(() => true, () => false);
|
|
180
|
+
try {
|
|
181
|
+
// verbatimSymlinks: otherwise Node rewrites links to absolute paths under
|
|
182
|
+
// `src`, and removing `src` below leaves the copies dangling.
|
|
183
|
+
await fs.cp(src, dest, { recursive: true, verbatimSymlinks: true });
|
|
184
|
+
}
|
|
185
|
+
catch (cpError) {
|
|
186
|
+
// A copy that dies partway (a file held open elsewhere) leaves half the
|
|
187
|
+
// source at `dest`, which reads as moved. `src` is still whole, so
|
|
188
|
+
// removing the copy leaves the plain "not moved" state. rm does not
|
|
189
|
+
// follow links, so a linked `src` copied as a link removes only that link.
|
|
190
|
+
if (!destExisted)
|
|
191
|
+
await fs.rm(dest, { recursive: true, force: true }).catch(() => { });
|
|
192
|
+
throw cpError;
|
|
193
|
+
}
|
|
124
194
|
// The same open handle that made rename fail with EPERM can fail this
|
|
125
195
|
// removal partway, after some files are already gone.
|
|
126
196
|
try {
|
|
@@ -164,21 +234,34 @@ async function appendDecisionLinks(ticketFile, archivedChangeDir, projectRoot) {
|
|
|
164
234
|
await fs.appendFile(ticketFile, block);
|
|
165
235
|
}
|
|
166
236
|
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
237
|
+
* Points a ticket's `ref:` line at the archived change, keeping the referenced
|
|
238
|
+
* filename. Returns the text unchanged when no line matches.
|
|
169
239
|
*
|
|
170
240
|
* Line endings survive untouched because the pattern excludes line terminators
|
|
171
241
|
* (`[^/\r\n]+`, no `s` flag), so `replace` only rewrites within a line. Do not
|
|
172
242
|
* "simplify" this by splitting and rejoining the file.
|
|
243
|
+
*
|
|
244
|
+
* `$2` runs straight to `$`: a `[ \t]*` between them split a blank run with
|
|
245
|
+
* `[^/\r\n]+` every way, and a filename holding 80k blanks took 13 s. That
|
|
246
|
+
* `[ \t]*` never matched anything, since the greedy capture reached `$` first,
|
|
247
|
+
* so trailing blanks stay in `$2` as before. `(?![ \t])` keeps `.*` from
|
|
248
|
+
* re-splitting the blanks after `ref:` (2 s on a line with no `/`): a shorter
|
|
249
|
+
* `$1` only hands `.*` blanks, never another `/`, so it could not match anyway.
|
|
250
|
+
*/
|
|
251
|
+
export function retargetRefLine(content, archiveName) {
|
|
252
|
+
return content.replace(/^(ref:[ \t]*)(?![ \t]).*\/([^/\r\n]+)$/m, `$1tospec/changes/archive/${archiveName}/$2`);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Repoints an archived ticket's `ref:` frontmatter at the change's new path,
|
|
256
|
+
* keeping the referenced filename. No-op when the line is absent.
|
|
173
257
|
*/
|
|
174
258
|
async function retargetTicketRef(ticketFile, archiveName) {
|
|
175
|
-
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
if (updated !== raw) {
|
|
259
|
+
// A rewrite emits UTF-8 without a BOM, as every tospec write does. Compared
|
|
260
|
+
// against the stripped text, as `writeTicketStub` does, so a ticket with no
|
|
261
|
+
// `ref:` to retarget is not rewritten just to drop its BOM.
|
|
262
|
+
const content = stripBom(await fs.readFile(ticketFile, 'utf-8'));
|
|
263
|
+
const updated = retargetRefLine(content, archiveName);
|
|
264
|
+
if (updated !== content) {
|
|
182
265
|
await fs.writeFile(ticketFile, updated);
|
|
183
266
|
}
|
|
184
267
|
}
|
|
@@ -267,6 +350,12 @@ export class ArchiveCommand {
|
|
|
267
350
|
if (opts.json) {
|
|
268
351
|
throw new ArchiveBlockedError(opts.code, opts.message, opts.fix);
|
|
269
352
|
}
|
|
353
|
+
// A prompt with no terminal reads EOF as Ctrl-C, so a CI run without
|
|
354
|
+
// `--yes` reported a cancellation (exit 130) instead of what it lacked.
|
|
355
|
+
// Same test as `config reset`, so the two refuse alike.
|
|
356
|
+
if (!isInteractive()) {
|
|
357
|
+
throw new ArchiveBlockedError(opts.code, `${opts.message} Cannot confirm it: no interactive terminal.`, opts.fix);
|
|
358
|
+
}
|
|
270
359
|
const { confirm } = await import('@inquirer/prompts');
|
|
271
360
|
const proceed = await confirm({ message: opts.prompt, default: false });
|
|
272
361
|
if (!proceed) {
|
|
@@ -299,6 +388,19 @@ export class ArchiveCommand {
|
|
|
299
388
|
return [];
|
|
300
389
|
}
|
|
301
390
|
}
|
|
391
|
+
/**
|
|
392
|
+
* Same rule as `findMissingRequiredArtifacts`: a schema that does not resolve
|
|
393
|
+
* yields nothing, because reporting it is validation's job, not this one's.
|
|
394
|
+
*/
|
|
395
|
+
findChangeSchemaWarnings(projectRoot, changeDir) {
|
|
396
|
+
try {
|
|
397
|
+
const schemaName = resolveSchemaForChange(changeDir, undefined, projectRoot);
|
|
398
|
+
return findSchemaWarnings(resolveSchema(schemaName, projectRoot));
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
return [];
|
|
402
|
+
}
|
|
403
|
+
}
|
|
302
404
|
/**
|
|
303
405
|
* Shared archive flow. Human mode prompts and returns null on cancellation;
|
|
304
406
|
* JSON mode keeps stdout clean and throws from every blocked path.
|
|
@@ -326,9 +428,10 @@ export class ArchiveCommand {
|
|
|
326
428
|
throw new ArchiveBlockedError('archive_change_name_required', 'A change name is required: archive --json is non-interactive.', 'tospec archive <change-name> --json');
|
|
327
429
|
}
|
|
328
430
|
// Without a terminal the picker cannot be answered: it would paint its
|
|
329
|
-
// menu into the pipe and then read EOF as "nothing chosen".
|
|
330
|
-
|
|
331
|
-
|
|
431
|
+
// menu into the pipe and then read EOF as "nothing chosen". `CI` counts
|
|
432
|
+
// as no terminal too, as it does for the confirmation prompts below.
|
|
433
|
+
if (!isInteractive()) {
|
|
434
|
+
throw new ArchiveBlockedError('archive_change_name_required', 'A change name is required: no interactive terminal to pick one.', 'tospec archive <change-name>');
|
|
332
435
|
}
|
|
333
436
|
const selectedChange = await this.selectChange(changesDir, root.path);
|
|
334
437
|
if (!selectedChange) {
|
|
@@ -345,6 +448,12 @@ export class ArchiveCommand {
|
|
|
345
448
|
if (nameFromArgument && !isKebabId(changeName)) {
|
|
346
449
|
throw new ArchiveBlockedError('archive_change_name_invalid', `Change name '${changeName}' is not a valid change id — it ${KEBAB_ID_DESCRIPTION}.`);
|
|
347
450
|
}
|
|
451
|
+
// Before the stat, which the archive directory passes: every gate then let
|
|
452
|
+
// it through, and only the final rename failed, as EINVAL, moving the
|
|
453
|
+
// directory into its own subdirectory.
|
|
454
|
+
if (isReservedChangeName(changeName)) {
|
|
455
|
+
throw new ArchiveBlockedError('archive_change_name_invalid', `${RESERVED_CHANGE_NAME_REASON}, and is not itself a change.`);
|
|
456
|
+
}
|
|
348
457
|
const changeDir = path.join(changesDir, changeName);
|
|
349
458
|
// Only a missing path may answer "not found" — a bare catch here rewrote
|
|
350
459
|
// genuine IO failures (EACCES, EIO) as a nonexistent change.
|
|
@@ -359,6 +468,17 @@ export class ArchiveCommand {
|
|
|
359
468
|
? `Change '${changeName}' not found. Available changes: ${available.join(', ')}`
|
|
360
469
|
: `Change '${changeName}' not found. No active changes exist in this root.`);
|
|
361
470
|
}
|
|
471
|
+
// Read before the move: `fs.rename` moves the link itself, so the archived
|
|
472
|
+
// entry is the same link and its content never lands in the archive.
|
|
473
|
+
const linkedTo = (await fs.lstat(changeDir)).isSymbolicLink() ? await fs.realpath(changeDir) : null;
|
|
474
|
+
// Every directory the move and the ticket move write into, checked before
|
|
475
|
+
// the first write: a file at one of them otherwise failed only after the
|
|
476
|
+
// specs merge, as a partly applied archive.
|
|
477
|
+
for (const dir of [archiveDir, path.join(root.path, 'tospec', 'tickets', 'archive')]) {
|
|
478
|
+
const occupied = findLayoutPathNotDirectory(root.path, dir);
|
|
479
|
+
if (occupied)
|
|
480
|
+
throw occupied;
|
|
481
|
+
}
|
|
362
482
|
// Reserve the archive name before anything is written. Checking after the
|
|
363
483
|
// merge left specs applied with the change still in tospec/changes/, so a
|
|
364
484
|
// retry re-applied the same deltas. The stamp is computed once and reused
|
|
@@ -404,16 +524,34 @@ export class ArchiveCommand {
|
|
|
404
524
|
const skipValidation = options.validate === false || options.noValidate === true;
|
|
405
525
|
// The exact pipeline `tospec validate` runs, minus the archive preflight —
|
|
406
526
|
// that is a dry run of the merge performed below, which raises
|
|
407
|
-
// archive_spec_update_failed itself
|
|
527
|
+
// archive_spec_update_failed itself — and minus the sister-change check,
|
|
528
|
+
// which would advise archiving the other change first; the scan after the
|
|
529
|
+
// merge below names the other change in the direction this run decided.
|
|
408
530
|
if (!skipValidation) {
|
|
409
531
|
const report = await validateChangeArtifacts(root.path, changeDir, {
|
|
410
532
|
archivePreflight: false,
|
|
533
|
+
sisterChanges: false,
|
|
411
534
|
});
|
|
412
535
|
if (!json && report.issues.length > 0) {
|
|
413
536
|
console.error(`\nValidation issues for change '${changeName}':`);
|
|
414
537
|
for (const issue of report.issues) {
|
|
415
538
|
const symbol = issueSymbol(issue.level);
|
|
416
|
-
console.error(` ${symbol} ${issue.path}: ${issue.message}`);
|
|
539
|
+
console.error(` ${symbol} ${issue.path}: ${indentContinuation(issue.message, ' ')}`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
// Human mode printed every finding above while --json printed none, so
|
|
543
|
+
// an agent archiving with --json never saw a WARNING (a dropped bullet,
|
|
544
|
+
// a missing SHALL) that a user at a terminal saw. ERRORs stay out:
|
|
545
|
+
// they fail the gate below, whose fix sends the reader to validate.
|
|
546
|
+
if (json) {
|
|
547
|
+
for (const issue of report.issues) {
|
|
548
|
+
if (issue.level !== 'WARNING')
|
|
549
|
+
continue;
|
|
550
|
+
jsonWarnings.push({
|
|
551
|
+
severity: 'warning',
|
|
552
|
+
code: 'archive_validation_warning',
|
|
553
|
+
message: `${issue.path}: ${issue.message}`,
|
|
554
|
+
});
|
|
417
555
|
}
|
|
418
556
|
}
|
|
419
557
|
if (!report.valid) {
|
|
@@ -422,15 +560,17 @@ export class ArchiveCommand {
|
|
|
422
560
|
// not at --no-validate.
|
|
423
561
|
const noDeltas = report.issues.some((i) => /at least one delta/i.test(i.message));
|
|
424
562
|
const deltaFix = noDeltas
|
|
425
|
-
? ` If this change
|
|
563
|
+
? ` If the user confirms this change needs no delta spec, declare it once by setting \`skip_specs: true\` in the change's ${METADATA_FILENAME} — do not invent a requirement to satisfy validation.`
|
|
426
564
|
: '';
|
|
427
565
|
if (json) {
|
|
428
566
|
throw new ArchiveBlockedError('archive_validation_failed', `Validation failed for change '${changeName}'.`, `Run ${`tospec validate ${changeName}`} for details, fix the errors, or rerun with --no-validate.${deltaFix}`);
|
|
429
567
|
}
|
|
430
|
-
|
|
568
|
+
// A line under `Error:` that is not `Fix:` is indented, here and at the
|
|
569
|
+
// merge aborts below: a bare line would read as a second message.
|
|
570
|
+
console.error('\nError: Validation failed. Please fix the errors before archiving.');
|
|
431
571
|
if (deltaFix)
|
|
432
572
|
console.error(`Fix:${deltaFix}`);
|
|
433
|
-
console.error('To skip validation (not recommended), use --no-validate flag.');
|
|
573
|
+
console.error(' To skip validation (not recommended), use --no-validate flag.');
|
|
434
574
|
process.exitCode = 1;
|
|
435
575
|
return null;
|
|
436
576
|
}
|
|
@@ -444,9 +584,11 @@ export class ArchiveCommand {
|
|
|
444
584
|
// when `--yes` overrides the gate. Remedies belong in `fix`, which an
|
|
445
585
|
// overridden gate drops.
|
|
446
586
|
message: `Validation is disabled for change '${changeName}' (--no-validate).`,
|
|
447
|
-
fix
|
|
448
|
-
|
|
449
|
-
|
|
587
|
+
// The human-mode fix also reaches a run with no terminal; `--json`
|
|
588
|
+
// there would swap the reader's prose output for a document.
|
|
589
|
+
fix: `tospec archive ${changeName}${json ? ' --json' : ''} --no-validate --yes`,
|
|
590
|
+
prompt: 'Warning: Skipping validation may archive invalid specs. Continue?',
|
|
591
|
+
warning: '\nWarning: Skipping validation may archive invalid specs.',
|
|
450
592
|
warnings: jsonWarnings,
|
|
451
593
|
});
|
|
452
594
|
if (!proceed)
|
|
@@ -480,6 +622,19 @@ export class ArchiveCommand {
|
|
|
480
622
|
if (!proceed)
|
|
481
623
|
return null;
|
|
482
624
|
}
|
|
625
|
+
// Ahead of the task gate: an unmatched `apply.tracks` sends progress to
|
|
626
|
+
// tasks.md, so the gate reports "no tracked task checkboxes" and asks for a
|
|
627
|
+
// file while the schema's real one sits in the change. The gate's decision
|
|
628
|
+
// stands; this is what makes its message make sense.
|
|
629
|
+
for (const warning of this.findChangeSchemaWarnings(root.path, changeDir)) {
|
|
630
|
+
if (json) {
|
|
631
|
+
jsonWarnings.push({ severity: 'warning', ...warning });
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
console.log(`Warning: ${warning.message}`);
|
|
635
|
+
console.log(`Fix: ${warning.fix}`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
483
638
|
// Pass the resolved root rather than re-deriving it from changesDir: the
|
|
484
639
|
// '..','..' hop silently encodes the tospec/changes layout.
|
|
485
640
|
const progress = await getTaskProgressForChange(changesDir, changeName, root.path);
|
|
@@ -517,18 +672,91 @@ export class ArchiveCommand {
|
|
|
517
672
|
return null;
|
|
518
673
|
}
|
|
519
674
|
let specsUpdated = false;
|
|
675
|
+
let mergedCapabilities = [];
|
|
520
676
|
let totals = { added: 0, modified: 0, removed: 0, renamed: 0, retired: 0 };
|
|
677
|
+
const specsDir = path.join(changeDir, 'specs');
|
|
678
|
+
const specUpdates = await findSpecUpdates(changeDir, mainSpecsDir);
|
|
679
|
+
// Where the merge-empty backstop below runs, it refuses and names every
|
|
680
|
+
// spec.md the merge cannot read. Everywhere else the merge drops those
|
|
681
|
+
// files, so the warnings below name them; keyed on the one condition, no
|
|
682
|
+
// file is both a warning and that error.
|
|
683
|
+
const backstopRuns = !options.skipSpecs && specUpdates.length === 0;
|
|
684
|
+
// Both outcomes, because the merge below can still stop the archive: a
|
|
685
|
+
// stopped one leaves the change in place, while a completed one has moved
|
|
686
|
+
// it to archive/, where it cannot be archived again.
|
|
687
|
+
const unmergedFix = 'To apply them: if this archive stopped, move each to specs/<capability>/spec.md and rerun; if it completed, write them as specs/<capability>/spec.md in a new change, or merge them into the main specs by hand.';
|
|
688
|
+
// The same list validate warns about, at the same level: a root `.md` with
|
|
689
|
+
// delta headers is a delta the merge will not read. Not added to the
|
|
690
|
+
// backstop below, which refuses — validate only warns here, and archive
|
|
691
|
+
// must not block what validate passes.
|
|
692
|
+
//
|
|
693
|
+
// Ahead of the --skip-specs branch, under one code for both: the file goes
|
|
694
|
+
// unmerged with or without the flag, so a consumer keyed on the code must
|
|
695
|
+
// not need to know which was passed. Not folded into
|
|
696
|
+
// archive_deltas_discarded, which lists the capabilities the flag dropped;
|
|
697
|
+
// findSpecUpdates never returns a root file, so no file is named twice.
|
|
698
|
+
//
|
|
699
|
+
// specs/spec.md joins the list only where the backstop does not run: under
|
|
700
|
+
// --skip-specs, or beside a capability delta, which gives the merge
|
|
701
|
+
// something to read. Validate's ERROR refuses a spec.md with a delta header
|
|
702
|
+
// first, so in practice this is --no-validate. Not added to that backstop
|
|
703
|
+
// instead: each flag is an explicit instruction, and the --skip-specs
|
|
704
|
+
// branch only warns about the deltas it drops.
|
|
705
|
+
const rootDeltas = await findRootDeltaFiles(specsDir, { includeRootSpec: !backstopRuns });
|
|
706
|
+
if (rootDeltas.length > 0) {
|
|
707
|
+
const listed = rootDeltas.map((file) => `specs/${path.basename(file)}`).join(', ');
|
|
708
|
+
const message = `${listed} hold(s) delta sections but sit at the specs/ root, so they are not merged into the main specs.`;
|
|
709
|
+
if (json) {
|
|
710
|
+
jsonWarnings.push({ severity: 'warning', code: 'archive_root_delta_ignored', message, fix: unmergedFix });
|
|
711
|
+
}
|
|
712
|
+
else {
|
|
713
|
+
console.log(`Warning: ${message}`);
|
|
714
|
+
console.log(`Fix: ${unmergedFix}`);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
// The delta files below the root that the merge does not read: a spec.md
|
|
718
|
+
// deeper than specs/<capability>/ or in a dot folder, and a misnamed file
|
|
719
|
+
// (specs/<capability>/notes.md, SPEC.md) with a delta header. They are the
|
|
720
|
+
// files the backstop lists when nothing merges; beside a capability delta
|
|
721
|
+
// or under --skip-specs they were dropped with nothing naming them.
|
|
722
|
+
// Validate refuses each of them first, so in practice this is
|
|
723
|
+
// --no-validate.
|
|
724
|
+
//
|
|
725
|
+
// A code of its own rather than archive_root_delta_ignored, whose name and
|
|
726
|
+
// message say "root", so a consumer reads the two codes as two different
|
|
727
|
+
// checks. A spec.md is picked by basename, as the backstop and validate's
|
|
728
|
+
// nested ERROR pick it. A misnamed file needs a delta header, the root's
|
|
729
|
+
// rule: without one it is notes.
|
|
730
|
+
if (!backstopRuns) {
|
|
731
|
+
const merged = new Set(specUpdates.map((update) => update.source));
|
|
732
|
+
const misplaced = [
|
|
733
|
+
...(await findSpecFiles(specsDir)).filter((file) => path.dirname(file) !== specsDir && !merged.has(file)),
|
|
734
|
+
...(await findMisnamedDeltaFiles(specsDir)),
|
|
735
|
+
].sort();
|
|
736
|
+
if (misplaced.length > 0) {
|
|
737
|
+
const listed = misplaced
|
|
738
|
+
.map((file) => `specs/${FileSystemUtils.toPosixPath(path.relative(specsDir, file))}`)
|
|
739
|
+
.join(', ');
|
|
740
|
+
const message = `${misplaced.length} delta spec file(s) are not merged into the main specs, because they do not sit at specs/<capability>/spec.md (one folder deep, not a dot folder): ${listed}.`;
|
|
741
|
+
if (json) {
|
|
742
|
+
jsonWarnings.push({ severity: 'warning', code: 'archive_misplaced_delta_ignored', message, fix: unmergedFix });
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
console.log(`Warning: ${message}`);
|
|
746
|
+
console.log(`Fix: ${unmergedFix}`);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
521
750
|
if (options.skipSpecs) {
|
|
522
751
|
// `--skip-specs` is for changes with no deltas, so pointing it at one that
|
|
523
752
|
// has them is user error — and archiving in silence made it unrecoverable.
|
|
524
753
|
// A warning, not a block, because the flag is an explicit instruction.
|
|
525
|
-
|
|
526
|
-
if (discarded.length > 0) {
|
|
754
|
+
if (specUpdates.length > 0) {
|
|
527
755
|
// source is <changeDir>/specs/<capability>/spec.md.
|
|
528
|
-
const listed =
|
|
756
|
+
const listed = specUpdates
|
|
529
757
|
.map((update) => path.basename(path.dirname(update.source)))
|
|
530
758
|
.join(', ');
|
|
531
|
-
const message = `--skip-specs discarded ${
|
|
759
|
+
const message = `--skip-specs discarded ${specUpdates.length} delta spec(s); these capabilities were not merged into the main specs: ${listed}.`;
|
|
532
760
|
if (json) {
|
|
533
761
|
jsonWarnings.push({ severity: 'warning', code: 'archive_deltas_discarded', message });
|
|
534
762
|
}
|
|
@@ -541,16 +769,30 @@ export class ArchiveCommand {
|
|
|
541
769
|
}
|
|
542
770
|
}
|
|
543
771
|
else {
|
|
544
|
-
const specUpdates = await findSpecUpdates(changeDir, mainSpecsDir);
|
|
545
772
|
if (specUpdates.length === 0) {
|
|
546
773
|
// findSpecUpdates only reads specs/<capability>/spec.md, and validation
|
|
547
774
|
// rejects anything else — so delta files here mean --no-validate, and
|
|
548
775
|
// reporting success would drop their requirements silently. Matches
|
|
549
|
-
// spec.md
|
|
550
|
-
|
|
776
|
+
// spec.md below the root, and another .md below the root only with a
|
|
777
|
+
// delta header: a stray NOTES.md is not a delta.
|
|
778
|
+
//
|
|
779
|
+
// specs/spec.md is picked by validate's own rule, under the name it has
|
|
780
|
+
// on disk: on a case-insensitive file system a lone SPEC.md is that
|
|
781
|
+
// ERROR's file. Without a delta header it counts only where the change
|
|
782
|
+
// must carry a delta; elsewhere validate leaves it as notes, and
|
|
783
|
+
// counting it anyway refused a change validate had passed while the
|
|
784
|
+
// Fix: sent the user back to validate.
|
|
785
|
+
const rootSpec = await findRootSpecDelta(specsDir, {
|
|
786
|
+
requireDelta: await changeRequiresDeltas(root.path, changeDir),
|
|
787
|
+
});
|
|
788
|
+
const orphans = [
|
|
789
|
+
...(await findSpecFiles(specsDir)).filter((file) => path.dirname(file) !== specsDir),
|
|
790
|
+
...(rootSpec === undefined ? [] : [rootSpec]),
|
|
791
|
+
...(await findMisnamedDeltaFiles(specsDir)),
|
|
792
|
+
].sort();
|
|
551
793
|
if (orphans.length > 0) {
|
|
552
794
|
const listed = orphans
|
|
553
|
-
.map((file) => FileSystemUtils.toPosixPath(path.relative(
|
|
795
|
+
.map((file) => FileSystemUtils.toPosixPath(path.relative(specsDir, file)))
|
|
554
796
|
.join(', ');
|
|
555
797
|
throw new ArchiveBlockedError('archive_spec_merge_empty', `Found ${orphans.length} delta spec file(s) that cannot be merged, because none sits at specs/<capability>/spec.md: ${listed}.`, `Run ${`tospec validate ${changeName}`} for the exact fix, or move each file to specs/<capability>/spec.md.`);
|
|
556
798
|
}
|
|
@@ -573,6 +815,12 @@ export class ArchiveCommand {
|
|
|
573
815
|
// gates, so a caller who only wanted the merge had to disarm every
|
|
574
816
|
// gate to get it.
|
|
575
817
|
if (!options.yes && !json) {
|
|
818
|
+
// Refuse rather than merge by default as JSON mode does: a human run
|
|
819
|
+
// was promised this question. Every gate has passed by this point,
|
|
820
|
+
// so a rerun with `--yes` overrides nothing but this prompt.
|
|
821
|
+
if (!isInteractive()) {
|
|
822
|
+
throw new ArchiveBlockedError('archive_confirmation_required', `Cannot confirm the spec updates for change '${changeName}': no interactive terminal.`, 'Rerun with --yes to merge them.');
|
|
823
|
+
}
|
|
576
824
|
const { confirm } = await import('@inquirer/prompts');
|
|
577
825
|
shouldUpdateSpecs = await confirm({
|
|
578
826
|
message: 'Proceed with spec updates?',
|
|
@@ -602,15 +850,28 @@ export class ArchiveCommand {
|
|
|
602
850
|
for (const update of specUpdates) {
|
|
603
851
|
const built = await buildUpdatedSpec(update, changeName, { silent: true });
|
|
604
852
|
mergeNotices.push(...built.notices);
|
|
605
|
-
|
|
853
|
+
// Inside the prepare pass, so a refused delete outside the
|
|
854
|
+
// project aborts before any spec is written.
|
|
855
|
+
const outside = await checkTargetWithinProject(update, root.path, built.retired);
|
|
856
|
+
if (outside)
|
|
857
|
+
mergeNotices.push(outside);
|
|
858
|
+
prepared.push({ update, rebuilt: built.rebuilt, crlf: built.crlf, retired: built.retired, counts: built.counts, original: built.original });
|
|
606
859
|
}
|
|
607
860
|
}
|
|
608
861
|
catch (err) {
|
|
862
|
+
const message = String(err.message || err);
|
|
863
|
+
// Each merge error is either the delta's or the main spec's; naming
|
|
864
|
+
// the delta for both sent the user to edit a file that was fine.
|
|
865
|
+
const fix = err instanceof MainSpecMergeError
|
|
866
|
+
? `Fix ${err.specPath} and rerun. No files were changed.`
|
|
867
|
+
: 'Fix the change delta specs and rerun. No files were changed.';
|
|
609
868
|
if (json) {
|
|
610
|
-
throw new ArchiveBlockedError('archive_spec_update_failed',
|
|
869
|
+
throw new ArchiveBlockedError('archive_spec_update_failed', message, fix);
|
|
611
870
|
}
|
|
612
|
-
|
|
613
|
-
|
|
871
|
+
// The diagnostic --json carries, as emitFailure would print it. Its
|
|
872
|
+
// fix already says no files were changed, so no `Aborted.` line.
|
|
873
|
+
console.error(formatLabeled('Error', message));
|
|
874
|
+
console.error(formatLabeled('Fix', fix));
|
|
614
875
|
process.exitCode = 1;
|
|
615
876
|
return writeTotalsSentinel;
|
|
616
877
|
}
|
|
@@ -624,36 +885,77 @@ export class ArchiveCommand {
|
|
|
624
885
|
if (p.retired)
|
|
625
886
|
continue;
|
|
626
887
|
const specName = path.basename(path.dirname(p.update.target));
|
|
627
|
-
const
|
|
628
|
-
if (
|
|
888
|
+
const failure = await new Validator().diagnoseRebuiltSpec(specName, p.rebuilt, p.original);
|
|
889
|
+
if (failure !== null) {
|
|
890
|
+
// The same attribution `tospec validate` uses for its preflight
|
|
891
|
+
// warning: a main spec that already fails is the file to edit,
|
|
892
|
+
// and sending the user to the delta sent them to a file that
|
|
893
|
+
// was fine.
|
|
894
|
+
const fix = failure.source === 'main'
|
|
895
|
+
? `Fix ${failure.specPath} and rerun. No files were changed.`
|
|
896
|
+
: `Run ${`tospec validate ${specName}`} after fixing the change deltas.`;
|
|
629
897
|
if (json) {
|
|
630
|
-
|
|
898
|
+
// The findings ride in the message, as the merge's own
|
|
899
|
+
// refusals carry theirs: a separate field would widen the
|
|
900
|
+
// status-entry contract every agent parses.
|
|
901
|
+
const findings = failure.issues
|
|
902
|
+
.filter((i) => i.level === 'ERROR')
|
|
903
|
+
.map((i) => ` ${i.path}: ${indentContinuation(i.message, ' ')}`);
|
|
904
|
+
const lead = failure.source === 'main'
|
|
905
|
+
? `Main spec ${failure.specPath} fails validation, so the spec rebuilt from it for '${specName}' would too. No files were changed.`
|
|
906
|
+
: `Rebuilt spec for '${specName}' failed validation. No files were changed.`;
|
|
907
|
+
throw new ArchiveBlockedError('archive_spec_validation_failed', [lead, ...findings].join('\n'), fix);
|
|
631
908
|
}
|
|
632
|
-
console.error(
|
|
633
|
-
|
|
909
|
+
console.error(failure.source === 'main'
|
|
910
|
+
? `\nError: Main spec ${failure.specPath} fails validation, so the rebuilt spec for ${specName} would too (will not write changes):`
|
|
911
|
+
: `\nError: Validation errors in rebuilt spec for ${specName} (will not write changes):`);
|
|
912
|
+
// A message's later lines (the scenario example some carry) sit
|
|
913
|
+
// under its text, not at the margin where they read as new lines.
|
|
914
|
+
for (const issue of failure.issues) {
|
|
634
915
|
if (issue.level === 'ERROR')
|
|
635
|
-
console.error(` ✗ ${issue.message}`);
|
|
916
|
+
console.error(` ✗ ${issue.path}: ${indentContinuation(issue.message, ' ')}`);
|
|
636
917
|
else if (issue.level === 'WARNING')
|
|
637
|
-
console.error(` ⚠ ${issue.message}`);
|
|
918
|
+
console.error(` ⚠ ${issue.path}: ${indentContinuation(issue.message, ' ')}`);
|
|
638
919
|
}
|
|
639
|
-
console.error('Aborted. No files were changed.');
|
|
920
|
+
console.error(' Aborted. No files were changed.');
|
|
921
|
+
console.error(formatLabeled('Fix', fix));
|
|
640
922
|
process.exitCode = 1;
|
|
641
923
|
return writeTotalsSentinel;
|
|
642
924
|
}
|
|
643
925
|
}
|
|
644
926
|
}
|
|
645
927
|
const writeTotals = { added: 0, modified: 0, removed: 0, renamed: 0, retired: 0 };
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
928
|
+
const capabilityOf = (p) => path.basename(path.dirname(p.update.target));
|
|
929
|
+
for (let i = 0; i < prepared.length; i++) {
|
|
930
|
+
const p = prepared[i];
|
|
931
|
+
// A write failing after an earlier one succeeded leaves the project
|
|
932
|
+
// half-merged, which a rerun cannot finish: the specs already
|
|
933
|
+
// written refuse the change's ADDED requirements.
|
|
934
|
+
try {
|
|
935
|
+
if (p.retired) {
|
|
936
|
+
await retireSpec(p.update, mainSpecsDir, { silent: json });
|
|
937
|
+
}
|
|
938
|
+
else {
|
|
939
|
+
await writeUpdatedSpec(p.update, p.rebuilt, p.counts, {
|
|
940
|
+
silent: json,
|
|
941
|
+
crlf: p.crlf,
|
|
942
|
+
});
|
|
943
|
+
}
|
|
650
944
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
945
|
+
catch (error) {
|
|
946
|
+
if (i === 0)
|
|
947
|
+
throw error;
|
|
948
|
+
throw partiallyAppliedError(error, {
|
|
949
|
+
projectRoot: root.path,
|
|
950
|
+
changeDir,
|
|
951
|
+
archivePath,
|
|
952
|
+
mergedCapabilities: prepared.slice(0, i).map(capabilityOf),
|
|
953
|
+
unwrittenCapabilities: prepared.slice(i).map(capabilityOf),
|
|
954
|
+
changeMoved: false,
|
|
655
955
|
});
|
|
656
956
|
}
|
|
957
|
+
if (p.retired)
|
|
958
|
+
writeTotals.retired += 1;
|
|
657
959
|
writeTotals.added += p.counts.added;
|
|
658
960
|
writeTotals.modified += p.counts.modified;
|
|
659
961
|
writeTotals.removed += p.counts.removed;
|
|
@@ -675,22 +977,147 @@ export class ArchiveCommand {
|
|
|
675
977
|
(writeTotals.retired ? `, retired ${writeTotals.retired}` : ''));
|
|
676
978
|
console.log('Specs updated successfully.');
|
|
677
979
|
}
|
|
678
|
-
return {
|
|
980
|
+
return {
|
|
981
|
+
totals: writeTotals,
|
|
982
|
+
capabilities: prepared.map(capabilityOf),
|
|
983
|
+
};
|
|
679
984
|
}, { operation: `archive ${changeName}` });
|
|
680
985
|
// The human-mode failure paths above already printed and set the exit code.
|
|
681
986
|
if (locked === writeTotalsSentinel)
|
|
682
987
|
return null;
|
|
683
988
|
specsUpdated = true;
|
|
684
989
|
totals = locked.totals;
|
|
990
|
+
mergedCapabilities = locked.capabilities;
|
|
991
|
+
// Only once the merge wrote: a skipped or refused merge replaced no
|
|
992
|
+
// text another change could have been written against. Warned, not
|
|
993
|
+
// refused, since nothing about this change is wrong.
|
|
994
|
+
for (const warning of await this.findStaleSisterRewrites(changesDir, changeName, specUpdates)) {
|
|
995
|
+
if (json) {
|
|
996
|
+
jsonWarnings.push(warning);
|
|
997
|
+
}
|
|
998
|
+
else {
|
|
999
|
+
console.error(`Warning: ${warning.message}`);
|
|
1000
|
+
console.error(`Fix: ${warning.fix}`);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
685
1003
|
}
|
|
686
1004
|
}
|
|
687
1005
|
}
|
|
688
1006
|
// Name and collision were settled at the reservation above.
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
//
|
|
692
|
-
//
|
|
693
|
-
|
|
1007
|
+
// From the spec write on, a failure leaves the project half-archived, and a
|
|
1008
|
+
// rerun cannot finish it: the change's ADDED requirements already exist in
|
|
1009
|
+
// the main specs, so the merge refuses them. Reported under its own code
|
|
1010
|
+
// with the manual remaining steps, because `archive_error` with
|
|
1011
|
+
// `archive: null` reads as "nothing changed".
|
|
1012
|
+
let changeMoved = false;
|
|
1013
|
+
try {
|
|
1014
|
+
await fs.mkdir(archiveDir, { recursive: true });
|
|
1015
|
+
await moveDirectory(changeDir, archivePath);
|
|
1016
|
+
changeMoved = true;
|
|
1017
|
+
await this.archiveTicket(root.path, changeName, archiveName, archivePath, json, jsonWarnings);
|
|
1018
|
+
}
|
|
1019
|
+
catch (error) {
|
|
1020
|
+
if (!specsUpdated && !changeMoved)
|
|
1021
|
+
throw error;
|
|
1022
|
+
// moveDirectory removes its partial copy, but that removal can fail too;
|
|
1023
|
+
// the fix names the leftover only when one is actually on disk.
|
|
1024
|
+
const partialCopyLeft = !changeMoved &&
|
|
1025
|
+
(await fs.lstat(archivePath).then(() => true, () => false));
|
|
1026
|
+
throw partiallyAppliedError(error, {
|
|
1027
|
+
projectRoot: root.path,
|
|
1028
|
+
changeDir,
|
|
1029
|
+
archivePath,
|
|
1030
|
+
mergedCapabilities,
|
|
1031
|
+
changeMoved,
|
|
1032
|
+
partialCopyLeft,
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
// Warned, not refused: list, status and validate all work on a linked
|
|
1036
|
+
// change, so archive finishing it is consistent; what it cannot do is bring
|
|
1037
|
+
// the content into the project, and that is the reader's to decide.
|
|
1038
|
+
if (linkedTo !== null) {
|
|
1039
|
+
const warning = {
|
|
1040
|
+
code: 'archive_change_linked',
|
|
1041
|
+
message: `Change '${changeName}' is a symlink or junction to ${linkedTo}. Archive moved the link, so tospec/changes/archive/${archiveName} still points there: its content stays outside tospec/changes/archive/ and git does not track it there.`,
|
|
1042
|
+
fix: `To keep the archived change in the project, replace tospec/changes/archive/${archiveName} with a copy of ${linkedTo}.`,
|
|
1043
|
+
};
|
|
1044
|
+
if (json) {
|
|
1045
|
+
jsonWarnings.push({ severity: 'warning', ...warning });
|
|
1046
|
+
}
|
|
1047
|
+
else {
|
|
1048
|
+
console.log(`Warning: ${warning.message}`);
|
|
1049
|
+
console.log(`Fix: ${warning.fix}`);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
if (!json) {
|
|
1053
|
+
console.log(`Change '${changeName}' archived as '${archiveName}'.`);
|
|
1054
|
+
}
|
|
1055
|
+
return {
|
|
1056
|
+
change: changeName,
|
|
1057
|
+
archivedAs: archiveName,
|
|
1058
|
+
path: archivePath,
|
|
1059
|
+
specsUpdated,
|
|
1060
|
+
totals,
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* One warning per MODIFIED / REMOVED / RENAMED in another in-progress change
|
|
1065
|
+
* on a requirement this change's merge just wrote. That delta restates the
|
|
1066
|
+
* text from before this archive, often copied from this change's own delta,
|
|
1067
|
+
* and sync may have rewritten the requirement since; archived as is, it
|
|
1068
|
+
* replaces the block and the edit is lost with nothing reporting it.
|
|
1069
|
+
*
|
|
1070
|
+
* Best effort, like the sister read it builds on: an unreadable delta is
|
|
1071
|
+
* skipped, as the archive it warns about has already succeeded.
|
|
1072
|
+
*/
|
|
1073
|
+
async findStaleSisterRewrites(changesDir, changeName, specUpdates) {
|
|
1074
|
+
const sisters = await listSisterChangeDeltas(changesDir, changeName);
|
|
1075
|
+
if (sisters.length === 0)
|
|
1076
|
+
return [];
|
|
1077
|
+
const warnings = [];
|
|
1078
|
+
for (const update of specUpdates) {
|
|
1079
|
+
// The delta's folder name, as the sister deltas spell theirs; the target
|
|
1080
|
+
// may carry the on-disk spelling of an existing capability instead.
|
|
1081
|
+
const capability = path.basename(path.dirname(update.source));
|
|
1082
|
+
let touches;
|
|
1083
|
+
try {
|
|
1084
|
+
touches = listDeltaTouches(parseDeltaSpec(await fs.readFile(update.source, 'utf-8')), [
|
|
1085
|
+
'ADDED',
|
|
1086
|
+
'MODIFIED',
|
|
1087
|
+
'REMOVED',
|
|
1088
|
+
'RENAMED',
|
|
1089
|
+
]);
|
|
1090
|
+
}
|
|
1091
|
+
catch {
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
for (const { change, sister } of findSharedRequirements(capability, touches, sisters, [
|
|
1095
|
+
'MODIFIED',
|
|
1096
|
+
'REMOVED',
|
|
1097
|
+
'RENAMED',
|
|
1098
|
+
])) {
|
|
1099
|
+
warnings.push({
|
|
1100
|
+
severity: 'warning',
|
|
1101
|
+
code: 'archive_sister_change_stale',
|
|
1102
|
+
target: change,
|
|
1103
|
+
// Only what the match proves: both deltas touch the requirement. The
|
|
1104
|
+
// sister's base text is unknown here — with a third change archived in
|
|
1105
|
+
// between, it predates the text this archive replaced — so the message
|
|
1106
|
+
// claims no base.
|
|
1107
|
+
message: `The ${sister.operation} "${sister.requirement}" in change '${change}' (specs/${capability}/spec.md) rewrites a requirement this archive just changed in tospec/specs/${capability}/spec.md; archived as is, it would replace this change's edit with its own text.`,
|
|
1108
|
+
fix: `Rewrite that delta in '${change}' against the updated tospec/specs/${capability}/spec.md before archiving '${change}'.`,
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
return warnings;
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Moves the change's ticket out of the active index. The ticket lives in the
|
|
1116
|
+
* global tospec/tickets/ index, outside the change dir, so it moves alongside
|
|
1117
|
+
* to keep the active index to in-flight changes.
|
|
1118
|
+
*/
|
|
1119
|
+
async archiveTicket(projectRoot, changeName, archiveName, archivePath, json, jsonWarnings) {
|
|
1120
|
+
const ticketPaths = await findTicketPaths(projectRoot, changeName);
|
|
694
1121
|
// Newest wins: a second ticket means the change was recreated, so the later
|
|
695
1122
|
// stamp describes the change being archived. The rest are reported, not
|
|
696
1123
|
// cleaned up — archive must not silently remove a record.
|
|
@@ -709,7 +1136,7 @@ export class ArchiveCommand {
|
|
|
709
1136
|
}
|
|
710
1137
|
}
|
|
711
1138
|
if (ticketPath) {
|
|
712
|
-
const ticketArchiveDir = path.join(
|
|
1139
|
+
const ticketArchiveDir = path.join(projectRoot, 'tospec', 'tickets', 'archive');
|
|
713
1140
|
await fs.mkdir(ticketArchiveDir, { recursive: true });
|
|
714
1141
|
const archivedTicketPath = path.join(ticketArchiveDir, path.basename(ticketPath));
|
|
715
1142
|
// `moveFile` overwrites, and two tickets share a filename when the same
|
|
@@ -737,19 +1164,9 @@ export class ArchiveCommand {
|
|
|
737
1164
|
else {
|
|
738
1165
|
await moveFile(ticketPath, archivedTicketPath);
|
|
739
1166
|
await retargetTicketRef(archivedTicketPath, archiveName);
|
|
740
|
-
await appendDecisionLinks(archivedTicketPath, archivePath,
|
|
1167
|
+
await appendDecisionLinks(archivedTicketPath, archivePath, projectRoot);
|
|
741
1168
|
}
|
|
742
1169
|
}
|
|
743
|
-
if (!json) {
|
|
744
|
-
console.log(`Change '${changeName}' archived as '${archiveName}'.`);
|
|
745
|
-
}
|
|
746
|
-
return {
|
|
747
|
-
change: changeName,
|
|
748
|
-
archivedAs: archiveName,
|
|
749
|
-
path: archivePath,
|
|
750
|
-
specsUpdated,
|
|
751
|
-
totals,
|
|
752
|
-
};
|
|
753
1170
|
}
|
|
754
1171
|
async selectChange(changesDir, projectRoot) {
|
|
755
1172
|
const { select } = await import('@inquirer/prompts');
|