@seanmars/tospec 0.24.0 → 0.26.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 +243 -0
- package/README.md +10 -6
- package/assets/dashboard/lib/derive.js +11 -5
- package/assets/dashboard/lib/store.js +4 -2
- package/assets/dashboard/views/change.js +5 -3
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +42 -7
- 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.d.ts +9 -0
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dashboard.js +77 -21
- 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 +3 -3
- package/dist/core/dashboard-hub.d.ts.map +1 -1
- package/dist/core/dashboard-hub.js +16 -4
- package/dist/core/dashboard-hub.js.map +1 -1
- package/dist/core/dashboard-routes.d.ts +2 -1
- package/dist/core/dashboard-routes.d.ts.map +1 -1
- package/dist/core/dashboard-routes.js +102 -33
- package/dist/core/dashboard-routes.js.map +1 -1
- 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 +116 -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 +9 -2
- package/dist/core/local-server.d.ts.map +1 -1
- package/dist/core/local-server.js +24 -4
- 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/index.d.ts +1 -1
- package/dist/core/shared/index.d.ts.map +1 -1
- package/dist/core/shared/index.js +1 -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 +66 -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 +4 -1
- 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
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { promises as fs } from 'fs';
|
|
3
3
|
import { Validator } from '../core/validation/validator.js';
|
|
4
|
-
import { issueSymbol } from '../core/validation/types.js';
|
|
4
|
+
import { issueSymbol, ARCHIVE_REFUSAL_CODE } from '../core/validation/types.js';
|
|
5
5
|
import { validateSections } from '../core/validation/section-validator.js';
|
|
6
6
|
import { resolveSchema, getSchemaDir } from '../core/artifact-graph/resolver.js';
|
|
7
7
|
import { resolveArtifactOutputs } from '../core/artifact-graph/outputs.js';
|
|
8
|
-
import {
|
|
9
|
-
import { METADATA_FILENAME, readSkipSpecsMarker, resolveSchemaForChange, } from '../utils/change-metadata.js';
|
|
8
|
+
import { detectStubArtifact } from '../core/artifact-graph/stub-detection.js';
|
|
9
|
+
import { ChangeMetadataError, METADATA_FILENAME, UnknownChangeSchemaError, readSkipSpecsMarker, resolveSchemaForChange, findChangeSchemaFault, findUnknownMetadataKeys, readChangeMetadata, suggestMetadataKey, } from '../utils/change-metadata.js';
|
|
10
|
+
import { findDecisionReferenceProblems } from './decision.js';
|
|
10
11
|
import { FileSystemUtils } from '../utils/file-system.js';
|
|
11
|
-
import {
|
|
12
|
+
import { findRootDeltaFiles, findSkipSpecsContradictions } from '../utils/spec-files.js';
|
|
12
13
|
import { resolveRootForCommand, toRootOutput, } from '../core/root-selection.js';
|
|
13
14
|
import { isInteractive, resolveNoInteractive } from '../utils/interactive.js';
|
|
14
|
-
import { emitSuccess, emitFailureStatus } from './shared-output.js';
|
|
15
|
+
import { emitSuccess, emitFailureStatus, indentContinuation } from './shared-output.js';
|
|
15
16
|
import { getSpecIds } from '../utils/item-discovery.js';
|
|
16
17
|
import { getAvailableChanges } from './workflow/shared.js';
|
|
17
18
|
import { nearestMatches } from '../utils/match.js';
|
|
18
19
|
import { resolveTaskFiles } from '../utils/task-progress.js';
|
|
19
20
|
import { findTaskNumberingIssues, } from '../core/validation/task-numbering.js';
|
|
21
|
+
import { findTaskMarkerIssues } from '../core/validation/task-markers.js';
|
|
20
22
|
/**
|
|
21
23
|
* The one null-shape every `validate --json` failure carries, so a caller
|
|
22
24
|
* reading `result.summary.totals` never gets `undefined` from one failure path
|
|
@@ -105,7 +107,7 @@ export class ValidateCommand {
|
|
|
105
107
|
items.push(...changes.map(id => ({ name: `change/${id}`, value: { type: 'change', id } })));
|
|
106
108
|
items.push(...specs.map(id => ({ name: `spec/${id}`, value: { type: 'spec', id } })));
|
|
107
109
|
if (items.length === 0) {
|
|
108
|
-
console.error('No items found to validate.');
|
|
110
|
+
console.error('Error: No items found to validate.');
|
|
109
111
|
process.exitCode = 1;
|
|
110
112
|
return;
|
|
111
113
|
}
|
|
@@ -132,10 +134,12 @@ export class ValidateCommand {
|
|
|
132
134
|
});
|
|
133
135
|
return;
|
|
134
136
|
}
|
|
135
|
-
|
|
137
|
+
// Every line after `Error:` is indented, as a multi-line fix is: a bare
|
|
138
|
+
// line would read as a second message.
|
|
139
|
+
console.error('Error: Nothing to validate. Try one of:');
|
|
136
140
|
for (const line of fix)
|
|
137
141
|
console.error(` ${line}`);
|
|
138
|
-
console.error('Or run in an interactive terminal.');
|
|
142
|
+
console.error(' Or run in an interactive terminal.');
|
|
139
143
|
process.exitCode = 1;
|
|
140
144
|
}
|
|
141
145
|
async validateDirectItem(root, itemName, opts) {
|
|
@@ -162,7 +166,7 @@ export class ValidateCommand {
|
|
|
162
166
|
emitFailureStatus({ ...VALIDATE_FAILURE_PAYLOAD, root: toRootOutput(root) }, { severity: 'error', code: 'unknown_item', message });
|
|
163
167
|
}
|
|
164
168
|
else {
|
|
165
|
-
console.error(message);
|
|
169
|
+
console.error(`Error: ${message}`);
|
|
166
170
|
process.exitCode = 1;
|
|
167
171
|
}
|
|
168
172
|
return;
|
|
@@ -178,7 +182,7 @@ export class ValidateCommand {
|
|
|
178
182
|
emitFailureStatus({ ...VALIDATE_FAILURE_PAYLOAD, root: toRootOutput(root) }, { severity: 'error', code: 'unknown_item', message });
|
|
179
183
|
}
|
|
180
184
|
else {
|
|
181
|
-
console.error(message);
|
|
185
|
+
console.error(`Error: ${message}`);
|
|
182
186
|
process.exitCode = 1;
|
|
183
187
|
}
|
|
184
188
|
return;
|
|
@@ -193,8 +197,8 @@ export class ValidateCommand {
|
|
|
193
197
|
});
|
|
194
198
|
return;
|
|
195
199
|
}
|
|
196
|
-
console.error(`Ambiguous item '${itemName}' matches both a change and a spec.`);
|
|
197
|
-
console.error('Pass --type change|spec, or use: tospec validate --changes / tospec validate --specs');
|
|
200
|
+
console.error(`Error: Ambiguous item '${itemName}' matches both a change and a spec.`);
|
|
201
|
+
console.error('Fix: Pass --type change|spec, or use: tospec validate --changes / tospec validate --specs');
|
|
198
202
|
process.exitCode = 1;
|
|
199
203
|
return;
|
|
200
204
|
}
|
|
@@ -220,6 +224,8 @@ export class ValidateCommand {
|
|
|
220
224
|
}
|
|
221
225
|
printReport(type, id, report, durationMs, json, root, strict = false) {
|
|
222
226
|
if (json) {
|
|
227
|
+
const archiveRefusals = countArchiveRefusals(report.issues);
|
|
228
|
+
const counts = { items: 1, passed: report.valid ? 1 : 0, failed: report.valid ? 0 : 1, archiveRefusals };
|
|
223
229
|
emitSuccess({
|
|
224
230
|
items: [
|
|
225
231
|
{
|
|
@@ -229,9 +235,10 @@ export class ValidateCommand {
|
|
|
229
235
|
issues: report.issues,
|
|
230
236
|
...(report.notes?.length ? { notes: report.notes } : {}),
|
|
231
237
|
durationMs,
|
|
238
|
+
archiveRefusals,
|
|
232
239
|
},
|
|
233
240
|
],
|
|
234
|
-
summary: { totals:
|
|
241
|
+
summary: { totals: counts, byType: { [type]: { ...counts } } },
|
|
235
242
|
}, toRootOutput(root));
|
|
236
243
|
return;
|
|
237
244
|
}
|
|
@@ -272,6 +279,19 @@ export class ValidateCommand {
|
|
|
272
279
|
printNextSteps(type, id, root, issues, strict) {
|
|
273
280
|
const bullets = [];
|
|
274
281
|
if (type === 'change') {
|
|
282
|
+
// The metadata and the whole-change "No deltas found" ERROR sit outside
|
|
283
|
+
// `<capability>/spec.md`, so the delta hints below never matched them,
|
|
284
|
+
// and a change failing on those too was told only to add a section.
|
|
285
|
+
// Their messages carry the fix; the bullet says it is still owed. It does
|
|
286
|
+
// not say "Fix .tospec.yaml": a schema issue filed there may be fixed in
|
|
287
|
+
// tospec/config.yaml or the schema's own file.
|
|
288
|
+
const fails = (issue) => issue.level === 'ERROR' || strict;
|
|
289
|
+
if (issues.some((issue) => issue.path === METADATA_FILENAME && fails(issue))) {
|
|
290
|
+
bullets.push(`- Resolve the ${METADATA_FILENAME} issue as its message above says`);
|
|
291
|
+
}
|
|
292
|
+
if (issues.some((issue) => issue.path === 'file' && /No deltas found/.test(issue.message) && fails(issue))) {
|
|
293
|
+
bullets.push('- Write the delta specs under specs/<capability>/spec.md, or set `skip_specs: true` in .tospec.yaml only when the user confirms this change needs no delta spec');
|
|
294
|
+
}
|
|
275
295
|
// Each delta hint answers one failure. A missing Migration field or a
|
|
276
296
|
// SHALL/MUST warning under --strict is a delta issue too, and used to be
|
|
277
297
|
// told to add a Scenario block it already had.
|
|
@@ -357,10 +377,10 @@ export class ValidateCommand {
|
|
|
357
377
|
}
|
|
358
378
|
if (queue.length === 0) {
|
|
359
379
|
const summary = {
|
|
360
|
-
totals: { items: 0, passed: 0, failed: 0 },
|
|
380
|
+
totals: { items: 0, passed: 0, failed: 0, archiveRefusals: 0 },
|
|
361
381
|
byType: {
|
|
362
|
-
...(scope.changes ? { change: { items: 0, passed: 0, failed: 0 } } : {}),
|
|
363
|
-
...(scope.specs ? { spec: { items: 0, passed: 0, failed: 0 } } : {}),
|
|
382
|
+
...(scope.changes ? { change: { items: 0, passed: 0, failed: 0, archiveRefusals: 0 } } : {}),
|
|
383
|
+
...(scope.specs ? { spec: { items: 0, passed: 0, failed: 0, archiveRefusals: 0 } } : {}),
|
|
364
384
|
},
|
|
365
385
|
};
|
|
366
386
|
if (opts.json) {
|
|
@@ -415,7 +435,7 @@ export class ValidateCommand {
|
|
|
415
435
|
const passed = results.filter((res) => res.valid).length;
|
|
416
436
|
const failed = results.length - passed;
|
|
417
437
|
const summary = {
|
|
418
|
-
totals: { items: results.length, passed, failed },
|
|
438
|
+
totals: { items: results.length, passed, failed, archiveRefusals: sumArchiveRefusals(results) },
|
|
419
439
|
byType: {
|
|
420
440
|
...(scope.changes ? { change: summarizeType(results, 'change') } : {}),
|
|
421
441
|
...(scope.specs ? { spec: summarizeType(results, 'spec') } : {}),
|
|
@@ -423,7 +443,10 @@ export class ValidateCommand {
|
|
|
423
443
|
};
|
|
424
444
|
// One copy per run: a sweep of forty specs trips the same rule forty times.
|
|
425
445
|
const runNotes = [...new Set(results.flatMap((res) => res.notes ?? []))];
|
|
426
|
-
const items = results.map(({ notes: _notes, ...res }) =>
|
|
446
|
+
const items = results.map(({ notes: _notes, ...res }) => ({
|
|
447
|
+
...res,
|
|
448
|
+
archiveRefusals: countArchiveRefusals(res.issues),
|
|
449
|
+
}));
|
|
427
450
|
if (opts.json) {
|
|
428
451
|
emitSuccess({ items, summary, ...(runNotes.length ? { notes: runNotes } : {}) }, toRootOutput(root));
|
|
429
452
|
}
|
|
@@ -475,17 +498,24 @@ export async function validateArtifactSections(projectRoot, changeDir) {
|
|
|
475
498
|
return issues;
|
|
476
499
|
}
|
|
477
500
|
const schemaDir = getSchemaDir(schemaName, projectRoot);
|
|
501
|
+
const changeName = path.basename(changeDir);
|
|
478
502
|
for (const artifact of schema.artifacts) {
|
|
479
503
|
for (const file of resolveArtifactOutputs(changeDir, artifact.generates)) {
|
|
480
504
|
const relPath = FileSystemUtils.toPosixPath(path.relative(changeDir, file));
|
|
481
505
|
// Runs before the section rules because a template satisfies them by
|
|
482
506
|
// construction — it is where every required heading came from. WARNING, so
|
|
483
507
|
// `--strict` is what refuses it.
|
|
484
|
-
|
|
508
|
+
// The change's own name, not a `<name>` placeholder: the message is the
|
|
509
|
+
// fix, and a placeholder makes the reader rebuild the command by hand.
|
|
510
|
+
const stub = detectStubArtifact(file, schemaName, artifact.template, projectRoot);
|
|
511
|
+
if (stub !== null) {
|
|
512
|
+
const instructions = `tospec instructions ${artifact.id} --change "${changeName}" --json`;
|
|
485
513
|
issues.push({
|
|
486
514
|
level: 'WARNING',
|
|
487
515
|
path: relPath,
|
|
488
|
-
message:
|
|
516
|
+
message: stub === 'empty'
|
|
517
|
+
? `Artifact "${artifact.id}" is empty. Run ${instructions}, then write the file's contents.`
|
|
518
|
+
: `Artifact "${artifact.id}" is still the unfilled template. Run ${instructions}, then replace the file's contents.`,
|
|
489
519
|
});
|
|
490
520
|
continue;
|
|
491
521
|
}
|
|
@@ -514,12 +544,13 @@ export async function validateArtifactSections(projectRoot, changeDir) {
|
|
|
514
544
|
}
|
|
515
545
|
const EMPTY_REPORT = { valid: true, issues: [], summary: { errors: 0, warnings: 0, info: 0 } };
|
|
516
546
|
/**
|
|
517
|
-
* Ambiguous task numbering
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
547
|
+
* Ambiguous task numbering and task lines the counter cannot see, as WARNINGs.
|
|
548
|
+
* Task IDs are how apply refers to work, so a duplicate makes that reference
|
|
549
|
+
* point at the wrong item; a `[~]` box drops out of the count, so the change
|
|
550
|
+
* reads done. Both are conventions, not structural requirements, so they report
|
|
551
|
+
* rather than block (`--strict` still refuses).
|
|
521
552
|
*/
|
|
522
|
-
async function
|
|
553
|
+
async function validateTaskFiles(projectRoot, changeDir) {
|
|
523
554
|
const documents = [];
|
|
524
555
|
for (const file of resolveTaskFiles(changeDir, projectRoot)) {
|
|
525
556
|
try {
|
|
@@ -532,10 +563,10 @@ async function validateTaskNumbering(projectRoot, changeDir) {
|
|
|
532
563
|
continue; // no tasks file yet — the artifact-status check owns that
|
|
533
564
|
}
|
|
534
565
|
}
|
|
535
|
-
return findTaskNumberingIssues(documents).map((issue) => ({
|
|
566
|
+
return [...findTaskNumberingIssues(documents), ...findTaskMarkerIssues(documents)].map((issue) => ({
|
|
536
567
|
level: 'WARNING',
|
|
537
568
|
path: issue.path,
|
|
538
|
-
line: issue.line,
|
|
569
|
+
...(issue.line !== undefined ? { line: issue.line } : {}),
|
|
539
570
|
message: issue.message,
|
|
540
571
|
}));
|
|
541
572
|
}
|
|
@@ -549,6 +580,13 @@ export async function validateChangeArtifacts(projectRoot, changeDir, options =
|
|
|
549
580
|
const validator = new Validator(strict);
|
|
550
581
|
const marker = readSkipSpecsMarker(changeDir);
|
|
551
582
|
const markerIssues = await skipSpecsMarkerIssues(changeDir, marker);
|
|
583
|
+
// Kept apart from markerIssues: those void the skip_specs waiver below, and
|
|
584
|
+
// a misspelled sibling key or a dangling ADR link does not contradict
|
|
585
|
+
// skip_specs. Skipped for a file that does not parse: the marker already
|
|
586
|
+
// reports it, and it has no keys to name.
|
|
587
|
+
const metadataFieldIssues = marker.unreadableReason
|
|
588
|
+
? []
|
|
589
|
+
: [...unknownMetadataKeyIssues(changeDir), ...decisionReferenceIssues(projectRoot, changeDir)];
|
|
552
590
|
// A schema that cannot be resolved is the one finding: every other check
|
|
553
591
|
// would run against `sdd` by default and report "no delta" about a change
|
|
554
592
|
// whose real problem is the schema name. Unparseable metadata is already
|
|
@@ -556,28 +594,65 @@ export async function validateChangeArtifacts(projectRoot, changeDir, options =
|
|
|
556
594
|
if (!marker.unreadableReason) {
|
|
557
595
|
const schemaIssue = schemaResolutionIssue(projectRoot, changeDir);
|
|
558
596
|
if (schemaIssue) {
|
|
559
|
-
return mergeSectionIssues(EMPTY_REPORT, [schemaIssue, ...markerIssues], strict);
|
|
597
|
+
return mergeSectionIssues(EMPTY_REPORT, [schemaIssue, ...markerIssues, ...metadataFieldIssues], strict);
|
|
560
598
|
}
|
|
561
599
|
}
|
|
562
|
-
|
|
563
|
-
//
|
|
564
|
-
//
|
|
565
|
-
//
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
600
|
+
const requireDeltas = await requiresDeltas(projectRoot, changeDir, marker, markerIssues);
|
|
601
|
+
// The delta pass also runs for a schema that demands no delta, whenever the
|
|
602
|
+
// change has a specs/ directory: archive merges every
|
|
603
|
+
// specs/<capability>/spec.md whatever the schema says. Gated on requireDeltas
|
|
604
|
+
// alone, such a schema merged deltas nobody had checked, and a nested or
|
|
605
|
+
// dot-folder spec.md passed here while the archive backstop refused it and
|
|
606
|
+
// sent the user back to validate. requireDeltas still decides whether a
|
|
607
|
+
// delta must exist.
|
|
608
|
+
const hasSpecsDir = await fs
|
|
609
|
+
.stat(path.join(changeDir, 'specs'))
|
|
610
|
+
.then((stat) => stat.isDirectory(), () => false);
|
|
611
|
+
const [deltaReport, sectionIssues, taskFileIssues, rootDeltaIssues] = await Promise.all([
|
|
612
|
+
requireDeltas || hasSpecsDir
|
|
573
613
|
? validator.validateChangeDeltaSpecs(changeDir, path.join(projectRoot, 'tospec', 'specs'), {
|
|
574
614
|
archivePreflight,
|
|
615
|
+
requireDelta: requireDeltas,
|
|
616
|
+
sisterChanges: options.sisterChanges,
|
|
575
617
|
})
|
|
576
618
|
: Promise.resolve(EMPTY_REPORT),
|
|
577
619
|
validateArtifactSections(projectRoot, changeDir),
|
|
578
|
-
|
|
620
|
+
validateTaskFiles(projectRoot, changeDir),
|
|
621
|
+
rootDeltaFileIssues(changeDir),
|
|
579
622
|
]);
|
|
580
|
-
return mergeSectionIssues(deltaReport, [...sectionIssues, ...
|
|
623
|
+
return mergeSectionIssues(deltaReport, [...sectionIssues, ...taskFileIssues, ...rootDeltaIssues, ...markerIssues, ...metadataFieldIssues], strict);
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* WARNING per delta written at `specs/<capability>.md`, which the merge never
|
|
627
|
+
* reads. Outside the `requireDeltas` gate on purpose: archive warns about the
|
|
628
|
+
* same list whatever the schema or skip_specs says, and validate running it
|
|
629
|
+
* only when a delta was demanded passed an issue-schema or skip_specs change
|
|
630
|
+
* that archive then flagged. WARNING, not ERROR, because a root notes file that
|
|
631
|
+
* quotes a delta header is legitimate; `--strict` still refuses it.
|
|
632
|
+
*
|
|
633
|
+
* `specs/spec.md` is left to the delta pass, which runs whatever the schema
|
|
634
|
+
* demands.
|
|
635
|
+
*/
|
|
636
|
+
async function rootDeltaFileIssues(changeDir) {
|
|
637
|
+
return (await findRootDeltaFiles(path.join(changeDir, 'specs'))).map((file) => {
|
|
638
|
+
const name = path.basename(file);
|
|
639
|
+
const capability = name.replace(/\.md$/i, '');
|
|
640
|
+
return {
|
|
641
|
+
level: 'WARNING',
|
|
642
|
+
path: name,
|
|
643
|
+
message: `specs/${name} holds delta sections, but a file at the specs/ root is ignored when the change is archived. Move it to specs/${capability}/spec.md.`,
|
|
644
|
+
};
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* `message` with trailing blanks dropped and ending in exactly one period.
|
|
649
|
+
* `trimEnd` strips exactly the `\s` set; the unanchored `/\.?\s*$/` it replaces
|
|
650
|
+
* retried from every position of an inner blank run, and a schema name holding
|
|
651
|
+
* 80k blanks took 2.8 s.
|
|
652
|
+
*/
|
|
653
|
+
export function endWithPeriod(message) {
|
|
654
|
+
const trimmed = message.trimEnd();
|
|
655
|
+
return trimmed.endsWith('.') ? trimmed : `${trimmed}.`;
|
|
581
656
|
}
|
|
582
657
|
/**
|
|
583
658
|
* The ERROR for a change whose schema cannot be resolved (`.tospec.yaml` names
|
|
@@ -586,18 +661,31 @@ export async function validateChangeArtifacts(projectRoot, changeDir, options =
|
|
|
586
661
|
* is wrong with the change.
|
|
587
662
|
*/
|
|
588
663
|
function schemaResolutionIssue(projectRoot, changeDir) {
|
|
589
|
-
|
|
590
|
-
|
|
664
|
+
const fault = findChangeSchemaFault(changeDir, projectRoot);
|
|
665
|
+
if (!fault)
|
|
591
666
|
return null;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
667
|
+
const { error } = fault;
|
|
668
|
+
const reason = endWithPeriod(error instanceof Error ? error.message : String(error));
|
|
669
|
+
// A bad field other than `schema` fails the same read, and its message
|
|
670
|
+
// already names the fix; telling the user to fix the schema name sent them
|
|
671
|
+
// to a line that was fine.
|
|
672
|
+
// A schema.yaml that does not read or parse is the same: the name found it,
|
|
673
|
+
// and the reason already names the file at fault.
|
|
674
|
+
// A name that came from tospec/config.yaml is not in .tospec.yaml at all, so
|
|
675
|
+
// the fix names the config field, in the words `new change` uses for it.
|
|
676
|
+
const fieldAtFault = error instanceof ChangeMetadataError && !(error instanceof UnknownChangeSchemaError);
|
|
677
|
+
const fix = fault.at === 'schema-file'
|
|
678
|
+
? 'Fix that schema file before anything else in this change can be checked.'
|
|
679
|
+
: fault.at === 'config'
|
|
680
|
+
? `Schema '${fault.schemaName}' comes from tospec/config.yaml; fix its 'schema:' field, or set 'schema:' in ${METADATA_FILENAME}, before anything else in this change can be checked.`
|
|
681
|
+
: fieldAtFault
|
|
682
|
+
? `Nothing else in this change can be checked until ${METADATA_FILENAME} reads.`
|
|
683
|
+
: `Fix the schema name in ${METADATA_FILENAME} before anything else in this change can be checked.`;
|
|
684
|
+
return {
|
|
685
|
+
level: 'ERROR',
|
|
686
|
+
path: METADATA_FILENAME,
|
|
687
|
+
message: `${reason} ${fix}`,
|
|
688
|
+
};
|
|
601
689
|
}
|
|
602
690
|
/**
|
|
603
691
|
* Errors in the skip_specs declaration itself: unparseable metadata, an unusable
|
|
@@ -607,7 +695,7 @@ function schemaResolutionIssue(projectRoot, changeDir) {
|
|
|
607
695
|
* Most-fundamental first and one at a time: a file that does not parse cannot
|
|
608
696
|
* also be said to hold a wrong value.
|
|
609
697
|
*/
|
|
610
|
-
async function skipSpecsMarkerIssues(changeDir, marker) {
|
|
698
|
+
async function skipSpecsMarkerIssues(changeDir, marker, contradictingSpecs) {
|
|
611
699
|
if (marker.unreadableReason) {
|
|
612
700
|
return [
|
|
613
701
|
{
|
|
@@ -622,27 +710,106 @@ async function skipSpecsMarkerIssues(changeDir, marker) {
|
|
|
622
710
|
{
|
|
623
711
|
level: 'ERROR',
|
|
624
712
|
path: METADATA_FILENAME,
|
|
625
|
-
message: `Invalid ${METADATA_FILENAME}: ${marker.invalidReason} Remove the key, or set it to true only when the change
|
|
713
|
+
message: `Invalid ${METADATA_FILENAME}: ${marker.invalidReason} Remove the key, or set it to true only when the user confirms this change needs no delta spec.`,
|
|
626
714
|
},
|
|
627
715
|
];
|
|
628
716
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
const
|
|
632
|
-
const specFiles = await findSpecFiles(specsDir);
|
|
717
|
+
// The same list `instructions apply` warns about. rootDeltaFileIssues covers
|
|
718
|
+
// the root `specs/<cap>.md` files it leaves out.
|
|
719
|
+
const specFiles = contradictingSpecs ?? (await findSkipSpecsContradictions(changeDir, marker));
|
|
633
720
|
if (specFiles.length === 0)
|
|
634
721
|
return [];
|
|
635
|
-
const listed = specFiles
|
|
636
|
-
.map((file) => FileSystemUtils.toPosixPath(path.relative(specsDir, file)))
|
|
637
|
-
.join(', ');
|
|
638
722
|
return [
|
|
639
723
|
{
|
|
640
724
|
level: 'ERROR',
|
|
641
725
|
path: METADATA_FILENAME,
|
|
642
|
-
message: `skip_specs: true declares this change has no delta specs, but ${specFiles.length} spec file(s) exist: ${
|
|
726
|
+
message: `skip_specs: true declares this change has no delta specs, but ${specFiles.length} spec file(s) exist: ${specFiles.join(', ')}. Drop skip_specs, or delete the spec files only when the user confirms this change needs no delta spec — never invent a requirement just to satisfy validation.`,
|
|
643
727
|
},
|
|
644
728
|
];
|
|
645
729
|
}
|
|
730
|
+
/**
|
|
731
|
+
* WARNING per top-level `.tospec.yaml` key the schema does not know. Not an
|
|
732
|
+
* ERROR: an unknown key breaks nothing on its own, but what it was meant to
|
|
733
|
+
* carry is dropped, and `--strict` still refuses it.
|
|
734
|
+
*/
|
|
735
|
+
function unknownMetadataKeyIssues(changeDir) {
|
|
736
|
+
return findUnknownMetadataKeys(changeDir).map((key) => {
|
|
737
|
+
const suggestion = suggestMetadataKey(key);
|
|
738
|
+
return {
|
|
739
|
+
level: 'WARNING',
|
|
740
|
+
path: METADATA_FILENAME,
|
|
741
|
+
message: `Unknown key "${printableKey(key)}" in ${METADATA_FILENAME} is ignored` +
|
|
742
|
+
(suggestion ? ` — did you mean "${suggestion}"?` : '.') +
|
|
743
|
+
' Keep data a tool stores for itself under an `x-` prefixed key (e.g. `x-mytool`), which tospec leaves alone.',
|
|
744
|
+
};
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* WARNING per `decisions:` value that is not a decision record filename or
|
|
749
|
+
* names no file under `tospec/decisions/` — the rules `new change --decisions`
|
|
750
|
+
* refuses by, applied to a value added by hand, which is how an ADR recorded
|
|
751
|
+
* before the change gets linked. Without it, archive turned such a value into
|
|
752
|
+
* a dead Related Decisions link.
|
|
753
|
+
*
|
|
754
|
+
* WARNING, as for an unknown key, not ERROR: archive's own use of the field is
|
|
755
|
+
* best-effort and never blocks, and a dangling link loses a reference without
|
|
756
|
+
* breaking anything the merge writes. `--strict` still refuses it.
|
|
757
|
+
*/
|
|
758
|
+
function decisionReferenceIssues(projectRoot, changeDir) {
|
|
759
|
+
let decisions;
|
|
760
|
+
try {
|
|
761
|
+
decisions = readChangeMetadata(changeDir, projectRoot)?.decisions;
|
|
762
|
+
}
|
|
763
|
+
catch {
|
|
764
|
+
return []; // schemaResolutionIssue reports a file that does not read
|
|
765
|
+
}
|
|
766
|
+
if (!decisions || decisions.length === 0)
|
|
767
|
+
return [];
|
|
768
|
+
const { notRecords, missing } = findDecisionReferenceProblems(projectRoot, decisions);
|
|
769
|
+
return [
|
|
770
|
+
...notRecords.map((value) => ({
|
|
771
|
+
level: 'WARNING',
|
|
772
|
+
path: METADATA_FILENAME,
|
|
773
|
+
message: `decisions: "${printableKey(value)}" is not a decision record filename. List the bare <yyyyMMdd_HHmmss>-<topic>.md name under tospec/decisions/, without a directory part; \`tospec decision list\` shows them.`,
|
|
774
|
+
})),
|
|
775
|
+
...missing.map((value) => ({
|
|
776
|
+
level: 'WARNING',
|
|
777
|
+
path: METADATA_FILENAME,
|
|
778
|
+
message: `decisions: "${printableKey(value)}" names no file in tospec/decisions/, so archive would link to nothing. Fix the name, or remove the entry.`,
|
|
779
|
+
})),
|
|
780
|
+
];
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* A key as it can be shown in a message: the file is user-controlled, so a
|
|
784
|
+
* control character or escape sequence in a key must not reach the terminal.
|
|
785
|
+
*/
|
|
786
|
+
function printableKey(key) {
|
|
787
|
+
const cleaned = key.replace(/[\u0000-\u001f\u007f-\u009f]/g, '?');
|
|
788
|
+
return cleaned.length > 80 ? `${cleaned.slice(0, 80)}…` : cleaned;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Whether this change must carry a delta spec, as validate decides it. The
|
|
792
|
+
* archive backstop asks too: it picks the files it refuses by the rule
|
|
793
|
+
* validate's ERRORs follow, and a `specs/spec.md` without a delta header is
|
|
794
|
+
* one of them only where a delta is required.
|
|
795
|
+
*/
|
|
796
|
+
export async function changeRequiresDeltas(projectRoot, changeDir, known) {
|
|
797
|
+
// `known` is for a caller that already read both, as `instructions apply`
|
|
798
|
+
// does: optional rather than a second export, so the decision keeps one body.
|
|
799
|
+
const marker = known?.marker ?? readSkipSpecsMarker(changeDir);
|
|
800
|
+
return requiresDeltas(projectRoot, changeDir, marker, await skipSpecsMarkerIssues(changeDir, marker, known?.contradictingSpecs));
|
|
801
|
+
}
|
|
802
|
+
async function requiresDeltas(projectRoot, changeDir, marker, markerIssues) {
|
|
803
|
+
// A declared marker waives the delta requirement; a *contradicted* one
|
|
804
|
+
// (declared, yet delta files exist) must not, because those files are real.
|
|
805
|
+
//
|
|
806
|
+
// Unparseable metadata waives it for the opposite reason: whether skip_specs
|
|
807
|
+
// was declared is unknowable, so "add a delta or set skip_specs" dead-ends a
|
|
808
|
+
// user who already set it inside the malformed file.
|
|
809
|
+
return (!marker.unreadableReason &&
|
|
810
|
+
(!marker.declared || markerIssues.length > 0) &&
|
|
811
|
+
(await deltaSpecsRequired(projectRoot, changeDir)));
|
|
812
|
+
}
|
|
646
813
|
/**
|
|
647
814
|
* Whether a change's schema requires at least one delta spec — a property of the
|
|
648
815
|
* schema, not of the change. False when the schema declares no `specs` artifact,
|
|
@@ -702,14 +869,26 @@ function describeTemplatePath(projectRoot, schemaDir, template) {
|
|
|
702
869
|
*/
|
|
703
870
|
export function formatIssueLine(issue, indent = '') {
|
|
704
871
|
const prefix = issueSymbol(issue.level);
|
|
705
|
-
|
|
872
|
+
// A message's later lines (a code frame, a scenario example) sit under its
|
|
873
|
+
// text: at the margin they read as the next finding.
|
|
874
|
+
return `${indent}${prefix} [${issue.level}] ${issue.path}: ${indentContinuation(issue.message, `${indent} `)}`;
|
|
706
875
|
}
|
|
707
876
|
function summarizeType(results, type) {
|
|
708
877
|
const filtered = results.filter(r => r.type === type);
|
|
709
878
|
const items = filtered.length;
|
|
710
879
|
const passed = filtered.filter(r => r.valid).length;
|
|
711
880
|
const failed = items - passed;
|
|
712
|
-
return { items, passed, failed };
|
|
881
|
+
return { items, passed, failed, archiveRefusals: sumArchiveRefusals(filtered) };
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Counted beside `passed` / `failed` because those cannot carry it: a refusal
|
|
885
|
+
* is a WARNING, so the item still passes and the exit code stays 0.
|
|
886
|
+
*/
|
|
887
|
+
function countArchiveRefusals(issues) {
|
|
888
|
+
return issues.filter((issue) => issue.code === ARCHIVE_REFUSAL_CODE).length;
|
|
889
|
+
}
|
|
890
|
+
function sumArchiveRefusals(results) {
|
|
891
|
+
return results.reduce((sum, res) => sum + countArchiveRefusals(res.issues), 0);
|
|
713
892
|
}
|
|
714
893
|
function normalizeConcurrency(value) {
|
|
715
894
|
if (!value)
|