mandrel 2.31.0 → 2.33.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/.agents/README.md +13 -17
- package/.agents/agents/acceptance-critic.md +1 -2
- package/.agents/docs/SDLC.md +12 -9
- package/.agents/docs/agentrc-reference.json +63 -58
- package/.agents/docs/configuration.md +275 -227
- package/.agents/docs/execution-reference.md +13 -14
- package/.agents/docs/quality-gates.md +195 -23
- package/.agents/instructions.md +2 -5
- package/.agents/rules/git-conventions-reference.md +27 -27
- package/.agents/rules/git-conventions.md +4 -2
- package/.agents/rules/known-tooling-behavior.md +66 -30
- package/.agents/rules/testing-standards.md +35 -71
- package/.agents/runtime-deps.json +2 -2
- package/.agents/schemas/agentrc.schema.json +1955 -1410
- package/.agents/schemas/lifecycle/README.md +21 -14
- package/.agents/schemas/lifecycle/ledger-record.schema.json +76 -22
- package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
- package/.agents/scripts/README.md +16 -29
- package/.agents/scripts/apply-quality-bootstrap.js +27 -34
- package/.agents/scripts/audit-to-stories.js +160 -41
- package/.agents/scripts/bootstrap.js +28 -26
- package/.agents/scripts/check-baseline-drift.js +73 -13
- package/.agents/scripts/check-baseline-scope.js +362 -0
- package/.agents/scripts/check-dead-exports.js +9 -1
- package/.agents/scripts/check-gherkin-corpus.js +508 -0
- package/.agents/scripts/check-knip-entries.js +159 -0
- package/.agents/scripts/check-lifecycle-lint.js +108 -124
- package/.agents/scripts/check-schema-references.js +1 -1
- package/.agents/scripts/diagnose-friction.js +7 -4
- package/.agents/scripts/generate-config-docs.js +263 -171
- package/.agents/scripts/install-matrix-assert.js +0 -1
- package/.agents/scripts/lib/ITicketingProvider.js +0 -58
- package/.agents/scripts/lib/audit-baselines/staleness.js +6 -6
- package/.agents/scripts/lib/audit-baselines/trend.js +7 -8
- package/.agents/scripts/lib/audit-baselines/weights.js +4 -5
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +1 -1
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +81 -35
- package/.agents/scripts/lib/audit-to-stories/wire-dependencies.js +185 -0
- package/.agents/scripts/lib/baselines/envelope.js +41 -60
- package/.agents/scripts/lib/baselines/git-base.js +30 -37
- package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
- package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
- package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
- package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
- package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
- package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
- package/.agents/scripts/lib/bdd-step-index.js +326 -0
- package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
- package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
- package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
- package/.agents/scripts/lib/checks/loop-health.js +9 -37
- package/.agents/scripts/lib/child-exec.js +193 -0
- package/.agents/scripts/lib/cli/standard-args.js +1 -1
- package/.agents/scripts/lib/cli-args.js +64 -0
- package/.agents/scripts/lib/close-validation/gates.js +2 -2
- package/.agents/scripts/lib/close-validation/runner.js +3 -3
- package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
- package/.agents/scripts/lib/config/commands.js +3 -5
- package/.agents/scripts/lib/config/explain.js +5 -7
- package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
- package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
- package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
- package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
- package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
- package/.agents/scripts/lib/config/gates/index.js +5 -2
- package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
- package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
- package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
- package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
- package/.agents/scripts/lib/config/gates/shared.js +58 -9
- package/.agents/scripts/lib/config/github.js +0 -1
- package/.agents/scripts/lib/config/limits.js +3 -48
- package/.agents/scripts/lib/config/qa.js +105 -0
- package/.agents/scripts/lib/config/runners.js +38 -16
- package/.agents/scripts/lib/config/temp-paths.js +6 -5
- package/.agents/scripts/lib/config-settings-schema-delivery.js +245 -56
- package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
- package/.agents/scripts/lib/config-settings-schema.js +386 -39
- package/.agents/scripts/lib/crap-baseline-join.js +126 -9
- package/.agents/scripts/lib/crap-utils.js +84 -520
- package/.agents/scripts/lib/dead-exports-knip.js +79 -10
- package/.agents/scripts/lib/degraded-mode.js +2 -2
- package/.agents/scripts/lib/dependency-parser.js +20 -7
- package/.agents/scripts/lib/doc-tiers.js +3 -3
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
- package/.agents/scripts/lib/findings/provenance-field.js +135 -0
- package/.agents/scripts/lib/findings/route-finding.js +57 -8
- package/.agents/scripts/lib/fs-walk.js +52 -0
- package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
- package/.agents/scripts/lib/git-utils.js +16 -36
- package/.agents/scripts/lib/knip-config-resolver.js +181 -0
- package/.agents/scripts/lib/knip-entry-sync.js +508 -0
- package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
- package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
- package/.agents/scripts/lib/observability/signal-validator.js +5 -85
- package/.agents/scripts/lib/observability/signals-writer.js +19 -62
- package/.agents/scripts/lib/observability/source-classifier.js +5 -7
- package/.agents/scripts/lib/observability/terse-result.js +3 -3
- package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
- package/.agents/scripts/lib/orchestration/code-review.js +2 -2
- package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
- package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
- package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
- package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
- package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
- package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +7 -26
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +69 -5
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +69 -13
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +49 -0
- package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
- package/.agents/scripts/lib/orchestration/resolve-stories.js +77 -50
- package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
- package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
- package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
- package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +59 -25
- package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
- package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
- package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
- package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
- package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +116 -1
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
- package/.agents/scripts/lib/orchestration/ticket-validator.js +38 -0
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
- package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
- package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
- package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
- package/.agents/scripts/lib/signals/detectors/common.js +1 -1
- package/.agents/scripts/lib/signals/index.js +8 -6
- package/.agents/scripts/lib/signals/schema.js +20 -25
- package/.agents/scripts/lib/signals/write.js +8 -8
- package/.agents/scripts/lib/story-body/footer-block.js +97 -0
- package/.agents/scripts/lib/story-body/story-body.js +18 -81
- package/.agents/scripts/lib/temp-retention.js +1 -1
- package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
- package/.agents/scripts/lib/ticket-body-sections.js +4 -5
- package/.agents/scripts/lib/wave-runner/footprint.js +306 -0
- package/.agents/scripts/lib/wave-runner/ready-set.js +198 -181
- package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
- package/.agents/scripts/lib/worktree-manager.js +2 -3
- package/.agents/scripts/lint-label-vocabulary.js +2 -24
- package/.agents/scripts/pr-watch-with-update.js +7 -5
- package/.agents/scripts/providers/github/blocked-by-add.js +25 -10
- package/.agents/scripts/providers/github/cache.js +2 -2
- package/.agents/scripts/providers/github/comments.js +6 -28
- package/.agents/scripts/providers/github/compose.js +0 -15
- package/.agents/scripts/providers/github/errors.js +10 -27
- package/.agents/scripts/providers/github/request-helpers.js +1 -2
- package/.agents/scripts/providers/github/sub-issues.js +10 -218
- package/.agents/scripts/providers/github.js +4 -7
- package/.agents/scripts/prune-baseline-orphans.js +181 -0
- package/.agents/scripts/resolve-stories.js +21 -7
- package/.agents/scripts/run-lint.js +61 -61
- package/.agents/scripts/run-test-profile.js +6 -6
- package/.agents/scripts/run-verify.js +48 -30
- package/.agents/scripts/single-story-close.js +20 -0
- package/.agents/scripts/single-story-init.js +12 -35
- package/.agents/scripts/stories-wave-tick.js +192 -9
- package/.agents/scripts/update-dead-exports-baseline.js +321 -0
- package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
- package/.agents/skills/skills.index.json +1 -11
- package/.agents/workflows/audit-documentation.md +5 -6
- package/.agents/workflows/audit-to-stories.md +28 -2
- package/.agents/workflows/helpers/audit-lens-core.md +11 -12
- package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
- package/.agents/workflows/helpers/code-review.md +3 -8
- package/.agents/workflows/helpers/deliver-reference.md +30 -2
- package/.agents/workflows/helpers/deliver-story-reference.md +84 -16
- package/.agents/workflows/helpers/plan-reference.md +76 -0
- package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
- package/.agents/workflows/mandrel-update.md +10 -10
- package/.agents/workflows/qa-assist.md +15 -20
- package/.agents/workflows/qa-explore.md +9 -8
- package/README.md +1 -1
- package/docs/CHANGELOG.md +58 -0
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
- package/package.json +14 -5
- package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
- package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
- package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
- package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
- package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
- package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
- package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
- package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
- package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
- package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
- package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
- package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
- package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
- package/.agents/scripts/check-gherkin-placeholders.js +0 -663
- package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
- package/.agents/scripts/lib/audit-suite/cli.js +0 -64
- package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
- package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
- package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
- package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
- package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
- package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
- package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
- package/.agents/scripts/lib/crap-baseline-index.js +0 -46
- package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
- package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
- package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
- package/.agents/scripts/lib/mutation/config-detector.js +0 -119
- package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
- package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
- package/.agents/scripts/lib/observability/active-story-env.js +0 -170
- package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
- package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
- package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
- package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
- package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
- package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
- package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
- package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
- package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
- package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
- package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
- package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
- package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
- package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
- package/.agents/scripts/lib/planning-corpus.js +0 -37
- package/.agents/scripts/lib/qa/coverage-report.js +0 -181
- package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
- package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
- package/.agents/scripts/lib/signals/detectors/index.js +0 -14
- package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
- package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
- package/.agents/scripts/lib/signals/read.js +0 -268
- package/.agents/scripts/lib/signals/span-tree.js +0 -291
- package/.agents/scripts/lib/story-lifecycle.js +0 -194
- package/.agents/scripts/lib/story-plan.js +0 -379
- package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
- package/.agents/scripts/lib/util/phase-timer.js +0 -163
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
- package/.agents/scripts/lint-baseline.js +0 -507
- package/.agents/scripts/providers/github/prs.js +0 -103
- package/.agents/scripts/signals-view.js +0 -309
- package/.agents/scripts/story-plan.js +0 -370
- package/.agents/scripts/sync-branch-from-base.js +0 -149
- package/.agents/scripts/validate-docs-freshness.js +0 -314
- package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
- package/.agents/workflows/helpers/signals.md +0 -112
|
@@ -75,11 +75,80 @@ export function readKnipOutput(filePath) {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Sentinel `symbol` recorded for a **whole-file death** row.
|
|
80
|
+
*
|
|
81
|
+
* Knip's `files` category names a module nothing in the graph imports. There
|
|
82
|
+
* is no per-export identity to record — the whole file is the finding — so the
|
|
83
|
+
* ratchet encodes exactly one row per dead file carrying this symbol. `*` is
|
|
84
|
+
* not a legal JavaScript identifier, so a whole-file row can never collide
|
|
85
|
+
* with a real export row for the same path.
|
|
86
|
+
*/
|
|
87
|
+
const WHOLE_FILE_SYMBOL = '*';
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Pull the dead-file paths out of one knip issue's `files` category.
|
|
91
|
+
*
|
|
92
|
+
* Knip emits `files: [{ name: '<path>' }]`, but tolerate a bare string and
|
|
93
|
+
* fall back to the issue's own `file` so a reporter-shape change degrades to
|
|
94
|
+
* "one row for this path" rather than to silence.
|
|
95
|
+
*
|
|
96
|
+
* @param {{ files?: unknown }} issue
|
|
97
|
+
* @param {string} fallbackFile The issue's own `file` path.
|
|
98
|
+
* @returns {string[]}
|
|
99
|
+
*/
|
|
100
|
+
function extractDeadFileNames(issue, fallbackFile) {
|
|
101
|
+
const entries = Array.isArray(issue.files) ? issue.files : [];
|
|
102
|
+
const names = [];
|
|
103
|
+
for (const entry of entries) {
|
|
104
|
+
const name =
|
|
105
|
+
(typeof entry === 'string' && entry) ||
|
|
106
|
+
(entry && typeof entry.name === 'string' && entry.name) ||
|
|
107
|
+
fallbackFile;
|
|
108
|
+
if (typeof name === 'string' && name.length > 0) names.push(name);
|
|
109
|
+
}
|
|
110
|
+
return names;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Pull the dead-export symbol names out of one knip issue's `exports`
|
|
115
|
+
* category. Knip emits `exports: [{ name, ... }]`; older shapes used `symbol`.
|
|
116
|
+
*
|
|
117
|
+
* @param {{ exports?: unknown }} issue
|
|
118
|
+
* @returns {string[]}
|
|
119
|
+
*/
|
|
120
|
+
function extractDeadExportSymbols(issue) {
|
|
121
|
+
const entries = Array.isArray(issue.exports) ? issue.exports : [];
|
|
122
|
+
const symbols = [];
|
|
123
|
+
for (const e of entries) {
|
|
124
|
+
const symbol =
|
|
125
|
+
(e && typeof e.name === 'string' && e.name) ||
|
|
126
|
+
(e && typeof e.symbol === 'string' && e.symbol) ||
|
|
127
|
+
null;
|
|
128
|
+
if (symbol) symbols.push(symbol);
|
|
129
|
+
}
|
|
130
|
+
return symbols;
|
|
131
|
+
}
|
|
132
|
+
|
|
78
133
|
/**
|
|
79
134
|
* Flatten knip's `--reporter json` output into `{ file, symbol }` rows. Knip
|
|
80
|
-
* emits `{ issues: [{ file, exports: [{ name, ... }], ... }
|
|
81
|
-
*
|
|
82
|
-
*
|
|
135
|
+
* emits `{ issues: [{ file, files: [...], exports: [{ name, ... }], ... }] }`.
|
|
136
|
+
*
|
|
137
|
+
* Two categories are mapped, and the ratchet treats their rows identically:
|
|
138
|
+
*
|
|
139
|
+
* - **`exports`** → one row per unused export, `{ file, symbol: '<name>' }`.
|
|
140
|
+
* - **`files`** → one row per module nothing imports,
|
|
141
|
+
* `{ file, symbol: '*' }`. Story #5001 added this leg: mapping only
|
|
142
|
+
* `exports` made the ratchet structurally blind to *whole-file* death, so a
|
|
143
|
+
* module could lose its last caller and every one of its exports go unused
|
|
144
|
+
* without a single row changing. Knip reports such a module once, under
|
|
145
|
+
* `files`, and suppresses its per-export rows — which is exactly why the
|
|
146
|
+
* export-only reading saw nothing.
|
|
147
|
+
*
|
|
148
|
+
* Whole-file rows are de-duplicated by path so the row set is stable across
|
|
149
|
+
* runs regardless of how many issue records mention the same file. Dependency-
|
|
150
|
+
* and duplicate-level issues stay ignored; knip surfaces those under their own
|
|
151
|
+
* `rules` keys and they are not a code-death signal.
|
|
83
152
|
*
|
|
84
153
|
* @param {unknown} knipEnvelope The parsed knip JSON report.
|
|
85
154
|
* @returns {Array<{ file: string, symbol: string }>}
|
|
@@ -88,16 +157,16 @@ export function extractRowsFromKnip(knipEnvelope) {
|
|
|
88
157
|
const rows = [];
|
|
89
158
|
if (!knipEnvelope || typeof knipEnvelope !== 'object') return rows;
|
|
90
159
|
const issues = Array.isArray(knipEnvelope.issues) ? knipEnvelope.issues : [];
|
|
160
|
+
const seenDeadFiles = new Set();
|
|
91
161
|
for (const issue of issues) {
|
|
92
162
|
const file = issue?.file;
|
|
93
163
|
if (typeof file !== 'string' || file.length === 0) continue;
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (!symbol) continue;
|
|
164
|
+
for (const name of extractDeadFileNames(issue, file)) {
|
|
165
|
+
if (seenDeadFiles.has(name)) continue;
|
|
166
|
+
seenDeadFiles.add(name);
|
|
167
|
+
rows.push({ file: name, symbol: WHOLE_FILE_SYMBOL });
|
|
168
|
+
}
|
|
169
|
+
for (const symbol of extractDeadExportSymbols(issue)) {
|
|
101
170
|
rows.push({ file, symbol });
|
|
102
171
|
}
|
|
103
172
|
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Tech Spec #819 §"Degraded-mode contract (Story 3)" — three soft-fail sites
|
|
5
5
|
* (audit-suite selector diff-timeout, lint-baseline JSON-parse, baseline-refresh
|
|
6
|
-
* guardrail git-diff) historically returned a silent zero/empty result.
|
|
7
|
-
*
|
|
6
|
+
* guardrail git-diff) historically returned a silent zero/empty result. Two
|
|
7
|
+
* remain; the lint-baseline site left with its CLI. The contract is:
|
|
8
8
|
*
|
|
9
9
|
* { ok: false, degraded: true, reason: <code>, detail: <human> }
|
|
10
10
|
*
|
|
@@ -7,18 +7,31 @@
|
|
|
7
7
|
* lib/story-adjacency.js, lib/branch-name-guard.js).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { parseFooterBlockedByIds } from './story-body/footer-block.js';
|
|
11
|
+
|
|
10
12
|
/**
|
|
11
|
-
* Parse
|
|
12
|
-
*
|
|
13
|
+
* Parse a body's declared blocker issue numbers — **footer-scoped and
|
|
14
|
+
* strict**.
|
|
15
|
+
*
|
|
16
|
+
* Only a `blocked by #N` line standing alone inside the `---` footer block
|
|
17
|
+
* declares an edge. The unanchored predecessor scanned the whole body for
|
|
18
|
+
* `blocked by|depends on #N` anywhere, so a Story whose prose merely mentioned
|
|
19
|
+
* a blocker — an example, a changelog note, an acceptance criterion describing
|
|
20
|
+
* this very defect — minted a real dispatch gate that withheld the Story until
|
|
21
|
+
* an unrelated issue closed.
|
|
22
|
+
*
|
|
23
|
+
* The behaviour change is deliberate and user-visible: prose-only mentions
|
|
24
|
+
* outside the footer no longer gate. Every machine-authored body already
|
|
25
|
+
* carries the canonical footer form (`plan-persist` has always serialized it),
|
|
26
|
+
* so only hand-written prose edges are affected — those must be moved into the
|
|
27
|
+
* footer block to keep gating. The grammar itself lives in
|
|
28
|
+
* `lib/story-body/footer-block.js`, shared with the body parser.
|
|
13
29
|
*
|
|
14
30
|
* @param {string} body - Issue body or freeform text.
|
|
15
|
-
* @returns {number[]} Array of issue numbers this
|
|
31
|
+
* @returns {number[]} Array of issue numbers this body declares as blockers.
|
|
16
32
|
*/
|
|
17
33
|
export function parseBlockedBy(body) {
|
|
18
|
-
|
|
19
|
-
const re = /(?:blocked\s+by|depends\s+on):?\s+#(\d+)/gi;
|
|
20
|
-
const ids = [...body.matchAll(re)].map((m) => Number.parseInt(m[1], 10));
|
|
21
|
-
return [...new Set(ids)];
|
|
34
|
+
return parseFooterBlockedByIds(body);
|
|
22
35
|
}
|
|
23
36
|
|
|
24
37
|
/**
|
|
@@ -201,9 +201,9 @@ export function resolveAlwaysLoadedClosure(root, { fs = nodeFs } = {}) {
|
|
|
201
201
|
|
|
202
202
|
/**
|
|
203
203
|
* Read the resolved `project.docsContextFiles` list from a config object,
|
|
204
|
-
* prefixed by `project.paths.docsRoot`.
|
|
205
|
-
* `
|
|
206
|
-
*
|
|
204
|
+
* prefixed by `project.paths.docsRoot`. Resolves new-shape
|
|
205
|
+
* `project.docsContextFiles` first and falls back to the legacy top-level
|
|
206
|
+
* key, so a config written against either shape yields the same list.
|
|
207
207
|
*
|
|
208
208
|
* @param {object} config resolved config (`resolveConfig()` output)
|
|
209
209
|
* @returns {string[]} repo-relative posix doc paths (existence not yet checked)
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* graduator-core.js — shared mechanism for the feedback-loop graduators.
|
|
3
3
|
*
|
|
4
|
-
* Story #3845 / Epic #3823. The audit-results
|
|
5
|
-
*
|
|
6
|
-
* (the since-retired `code-review-graduator.js`) duplicated ~90% of their mechanism: a
|
|
4
|
+
* Story #3845 / Epic #3823. The since-retired audit-results and code-review
|
|
5
|
+
* graduators duplicated ~90% of their mechanism: a
|
|
7
6
|
* `spawn`-based child runner, the `git cat-file` path probe, the
|
|
8
7
|
* `gh search issues` idempotency probe, the `gh issue create` filer, the
|
|
9
8
|
* `isAutoFileEnabled` toggle reader, and the route → probe → file
|
|
10
|
-
* envelope walk. This module folds all of that into one place so
|
|
11
|
-
*
|
|
12
|
-
* differ (the
|
|
9
|
+
* envelope walk. This module folds all of that into one place so a
|
|
10
|
+
* graduator becomes a thin shell that injects the bits that genuinely
|
|
11
|
+
* differ (the body/title/label builders).
|
|
12
|
+
*
|
|
13
|
+
* Story #5003 removed the structured-comment read limb. Both comment-reading
|
|
14
|
+
* graduators were Epic-era — they walked an Epic's `verification-results`
|
|
15
|
+
* comment, and v2 has no Epics — leaving `retro-proposals-graduator.js` and
|
|
16
|
+
* its pre-parsed `findings[]` as the one live caller.
|
|
13
17
|
*
|
|
14
18
|
* The third `runGh` spawn copy in `prior-feedback-fetcher.js` is also
|
|
15
19
|
* collapsed onto the single `runChild` helper here.
|
|
@@ -34,11 +38,10 @@
|
|
|
34
38
|
* - **Per-run filing cap.** `graduate()` stops filing once
|
|
35
39
|
* `maxFilingsPerRun` issues are created and records the excess as
|
|
36
40
|
* skipped `cap-reached`.
|
|
37
|
-
* - **Pre-parsed / path-less seam.** `graduate()`
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* consumes.
|
|
41
|
+
* - **Pre-parsed / path-less seam.** `graduate()` takes its findings as a
|
|
42
|
+
* pre-parsed array, and a path-less finding skips the path-exists gate
|
|
43
|
+
* instead of being misclassified `file-removed` — the seam the retro
|
|
44
|
+
* auto-filer consumes.
|
|
42
45
|
* - **Durable cross-repo deferral.** Cross-repo-deferred findings are
|
|
43
46
|
* upserted into a structured comment on the Epic instead of only a
|
|
44
47
|
* log line.
|
|
@@ -50,34 +53,7 @@ import { createHash } from 'node:crypto';
|
|
|
50
53
|
import { inNodeTestContext } from '../config/temp-paths.js';
|
|
51
54
|
import { LABEL_COLORS } from '../label-constants.js';
|
|
52
55
|
import { classifyPathSource as defaultClassifier } from '../observability/source-classifier.js';
|
|
53
|
-
import {
|
|
54
|
-
structuredCommentMarker,
|
|
55
|
-
upsertStructuredComment,
|
|
56
|
-
} from '../orchestration/ticketing.js';
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* The single structured-comment marker for the unified `verification-results`
|
|
60
|
-
* findings contract (Story #4411, Epic #4405). It replaces the two retired
|
|
61
|
-
* per-graduator markers (the former code-review and audit-results
|
|
62
|
-
* structured-comment markers). Both feedback-loop graduators
|
|
63
|
-
* search the Epic's comments for this one marker so they file follow-ups
|
|
64
|
-
* from the same unified source comment that `runCodeReview` upserts (comment
|
|
65
|
-
* type `verification-results`). Derived from the canonical
|
|
66
|
-
* `structuredCommentMarker` builder so the read-side marker stays byte-stable
|
|
67
|
-
* with the write side rather than being hand-copied.
|
|
68
|
-
*/
|
|
69
|
-
export const VERIFICATION_RESULTS_MARKER = structuredCommentMarker(
|
|
70
|
-
'verification-results',
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* The single "no source comment" skip reason for the unified contract. Both
|
|
75
|
-
* graduators surface this reason when the Epic carries no
|
|
76
|
-
* `verification-results` comment, replacing the two retired
|
|
77
|
-
* `no-code-review-comment` / `no-audit-results-comment` reasons.
|
|
78
|
-
*/
|
|
79
|
-
export const NO_VERIFICATION_RESULTS_COMMENT_REASON =
|
|
80
|
-
'no-verification-results-comment';
|
|
56
|
+
import { upsertStructuredComment } from '../orchestration/ticketing.js';
|
|
81
57
|
|
|
82
58
|
/**
|
|
83
59
|
* Default child-process timeout. A hung `gh`/`git` spawn previously blocked
|
|
@@ -814,28 +790,24 @@ export async function createFollowUpIssue({
|
|
|
814
790
|
}
|
|
815
791
|
|
|
816
792
|
/**
|
|
817
|
-
* Validate the `graduate` preconditions (toggle, epicId,
|
|
818
|
-
*
|
|
819
|
-
*
|
|
820
|
-
*
|
|
821
|
-
*
|
|
793
|
+
* Validate the `graduate` preconditions (toggle, epicId, currentRepo shape).
|
|
794
|
+
* Returns `null` when all preconditions pass, or a `{ skipped?, errors? }`
|
|
795
|
+
* partial-envelope the caller short-circuits on. Story #4075 — extracted from
|
|
796
|
+
* `graduate` so the orchestrating body holds no guard-chain branching.
|
|
797
|
+
*
|
|
798
|
+
* Story #5003 dropped the `provider.getTicketComments` gate with the
|
|
799
|
+
* structured-comment read limb. The provider is now consulted only by the
|
|
800
|
+
* best-effort cross-repo-deferred upsert, which reports its own faults into
|
|
801
|
+
* `errors[]` — gating the whole walk on a shape only that path needs would
|
|
802
|
+
* refuse work the walk can complete.
|
|
822
803
|
*/
|
|
823
|
-
function checkGraduatePreconditions({
|
|
824
|
-
epicId,
|
|
825
|
-
provider,
|
|
826
|
-
currentRepo,
|
|
827
|
-
config,
|
|
828
|
-
spec,
|
|
829
|
-
}) {
|
|
804
|
+
function checkGraduatePreconditions({ epicId, currentRepo, config, spec }) {
|
|
830
805
|
if (!spec.isAutoFileEnabled(config)) {
|
|
831
806
|
return { skipped: [{ reason: 'toggle-disabled' }] };
|
|
832
807
|
}
|
|
833
808
|
if (!Number.isInteger(epicId) || epicId < 1) {
|
|
834
809
|
return { errors: [`${spec.fnName}: missing or invalid epicId`] };
|
|
835
810
|
}
|
|
836
|
-
if (!provider || typeof provider.getTicketComments !== 'function') {
|
|
837
|
-
return { errors: [`${spec.fnName}: provider lacks getTicketComments`] };
|
|
838
|
-
}
|
|
839
811
|
if (
|
|
840
812
|
!currentRepo ||
|
|
841
813
|
typeof currentRepo.owner !== 'string' ||
|
|
@@ -846,36 +818,6 @@ function checkGraduatePreconditions({
|
|
|
846
818
|
return null;
|
|
847
819
|
}
|
|
848
820
|
|
|
849
|
-
/**
|
|
850
|
-
* Read the source structured comment off the Epic and parse its findings.
|
|
851
|
-
* Returns `{ findings }` on success, or `{ skipped?, errors? }` for the
|
|
852
|
-
* no-comment / parse-empty / fetch-error short-circuits. Story #4075 —
|
|
853
|
-
* extracted from `graduate`.
|
|
854
|
-
*/
|
|
855
|
-
async function loadGraduateFindings({ epicId, provider, spec }) {
|
|
856
|
-
let comments;
|
|
857
|
-
try {
|
|
858
|
-
comments = await provider.getTicketComments(epicId);
|
|
859
|
-
} catch (err) {
|
|
860
|
-
return {
|
|
861
|
-
errors: [
|
|
862
|
-
`getTicketComments failed for epic #${epicId}: ${err?.message ?? err}`,
|
|
863
|
-
],
|
|
864
|
-
};
|
|
865
|
-
}
|
|
866
|
-
const matched = (Array.isArray(comments) ? comments : []).filter(
|
|
867
|
-
(c) => typeof c?.body === 'string' && c.body.includes(spec.commentMarker),
|
|
868
|
-
);
|
|
869
|
-
if (matched.length === 0) {
|
|
870
|
-
return { skipped: [{ reason: spec.noCommentReason }] };
|
|
871
|
-
}
|
|
872
|
-
const findings = spec.parseFindings(matched[matched.length - 1].body);
|
|
873
|
-
if (findings.length === 0) {
|
|
874
|
-
return { skipped: [{ reason: 'no-non-blocking-findings' }] };
|
|
875
|
-
}
|
|
876
|
-
return { findings };
|
|
877
|
-
}
|
|
878
|
-
|
|
879
821
|
/**
|
|
880
822
|
* Probe whether a finding was already filed, checking both the current
|
|
881
823
|
* content-hash marker AND the legacy `(epicId, parse-index)` marker so
|
|
@@ -1267,17 +1209,16 @@ async function persistCrossRepoDeferred({
|
|
|
1267
1209
|
}
|
|
1268
1210
|
|
|
1269
1211
|
/**
|
|
1270
|
-
* Parametrized graduator walk.
|
|
1271
|
-
*
|
|
1272
|
-
* each finding runs the shared route → path probe → idempotency probe →
|
|
1212
|
+
* Parametrized graduator walk. Takes a pre-parsed `findings` array and, for
|
|
1213
|
+
* each finding, runs the shared route → path probe → idempotency probe →
|
|
1273
1214
|
* cap → file sequence. Never throws — every failure path is captured in
|
|
1274
1215
|
* `errors[]`.
|
|
1275
1216
|
*
|
|
1217
|
+
* Each finding MUST carry `{ severity, path, summary, index }` and MAY carry
|
|
1218
|
+
* additional fields (e.g. `category`) that the builders use.
|
|
1219
|
+
*
|
|
1276
1220
|
* The per-graduator variation lives entirely in the injected callbacks:
|
|
1277
1221
|
*
|
|
1278
|
-
* - `parseFindings(body)` — turns the rendered comment into findings.
|
|
1279
|
-
* Each finding MUST carry `{ severity, path, summary, index }` and
|
|
1280
|
-
* MAY carry additional fields (e.g. `lens`) that the builder uses.
|
|
1281
1222
|
* - `buildContentMarker(epicId, finding)` — the content-hash HTML-comment
|
|
1282
1223
|
* marker embedded in (and searched for in) follow-up bodies.
|
|
1283
1224
|
* - `buildLegacyMarker(epicId, index)` — the pre-cutover ordinal marker,
|
|
@@ -1299,7 +1240,8 @@ async function persistCrossRepoDeferred({
|
|
|
1299
1240
|
*
|
|
1300
1241
|
* @param {object} opts
|
|
1301
1242
|
* @param {number} opts.epicId
|
|
1302
|
-
* @param {object} opts.provider — exposes `
|
|
1243
|
+
* @param {object} opts.provider — exposes `postComment(ticketId, body)` for
|
|
1244
|
+
* the durable cross-repo-deferred persistence
|
|
1303
1245
|
* @param {object} [opts.config]
|
|
1304
1246
|
* @param {{owner: string, repo: string}} opts.currentRepo
|
|
1305
1247
|
* @param {{owner: string, repo: string}} [opts.frameworkRepo]
|
|
@@ -1310,9 +1252,9 @@ async function persistCrossRepoDeferred({
|
|
|
1310
1252
|
* @param {string} [opts.cwd]
|
|
1311
1253
|
* @param {number} [opts.timeoutMs] — per-spawn watchdog bound
|
|
1312
1254
|
* @param {number} [opts.maxFilingsPerRun] — per-run filing cap
|
|
1313
|
-
* @param {Array<object>}
|
|
1314
|
-
*
|
|
1315
|
-
*
|
|
1255
|
+
* @param {Array<object>} opts.findings — the pre-parsed findings to file.
|
|
1256
|
+
* Required: a non-array is an `errors[]` short-circuit, never a silent
|
|
1257
|
+
* no-op.
|
|
1316
1258
|
* @param {Set<string>} [opts.filedMarkers] — in-process memo of content
|
|
1317
1259
|
* markers filed so far. Pass a shared Set across multiple `graduate()`
|
|
1318
1260
|
* calls in one logical invocation (e.g. the retro graduator's two source
|
|
@@ -1360,23 +1302,23 @@ export async function graduate({
|
|
|
1360
1302
|
|
|
1361
1303
|
const precondition = checkGraduatePreconditions({
|
|
1362
1304
|
epicId,
|
|
1363
|
-
provider,
|
|
1364
1305
|
currentRepo,
|
|
1365
1306
|
config,
|
|
1366
1307
|
spec,
|
|
1367
1308
|
});
|
|
1368
1309
|
if (precondition) return { ...envelope, ...precondition };
|
|
1369
1310
|
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1311
|
+
// Pre-parsed findings are the only source (Story #5003). The
|
|
1312
|
+
// structured-comment read/parse limb went with the audit-results
|
|
1313
|
+
// graduator: it walked an Epic's `verification-results` comment, and v2
|
|
1314
|
+
// has no Epics, so it could only ever resolve zero findings.
|
|
1315
|
+
if (!Array.isArray(preParsedFindings)) {
|
|
1316
|
+
return {
|
|
1317
|
+
...envelope,
|
|
1318
|
+
errors: [`${spec.fnName}: findings[] is required and must be an array`],
|
|
1319
|
+
};
|
|
1379
1320
|
}
|
|
1321
|
+
const findings = preParsedFindings;
|
|
1380
1322
|
|
|
1381
1323
|
// Blast-radius guard (Story #4837): decided ONCE per walk, before the
|
|
1382
1324
|
// first spawn, so a refused context costs no child process at all.
|
|
@@ -138,6 +138,14 @@ function metaSourceLabel(source) {
|
|
|
138
138
|
* a single source so the constant classifier routes every finding in the
|
|
139
139
|
* bucket to the correct repo (and the label reflects that source).
|
|
140
140
|
*
|
|
141
|
+
* The bundle carries **only** builders. It used to declare a
|
|
142
|
+
* `commentMarker` / `noCommentReason` / `parseFindings` trio purely to satisfy
|
|
143
|
+
* the shared walk's shape — this graduator supplies its findings pre-parsed,
|
|
144
|
+
* so the walk never consulted any of the three. Story #5003 deleted that limb
|
|
145
|
+
* from `graduate()` along with the Epic-era graduator that was its only real
|
|
146
|
+
* consumer, and the placeholder fields went with it: a declared-but-unread
|
|
147
|
+
* field is a standing invitation to write a parser nothing will call.
|
|
148
|
+
*
|
|
141
149
|
* @param {'framework'|'consumer'} source
|
|
142
150
|
* @returns {object}
|
|
143
151
|
*/
|
|
@@ -145,12 +153,6 @@ function makeSpec(source) {
|
|
|
145
153
|
return {
|
|
146
154
|
fnName: 'graduateRetroProposals',
|
|
147
155
|
isAutoFileEnabled,
|
|
148
|
-
// Pre-parsed seam: the comment marker / parser are never consulted
|
|
149
|
-
// (findings are supplied directly), but the fields are declared for the
|
|
150
|
-
// shared walk's shape.
|
|
151
|
-
commentMarker: '<!-- structured-comment: retro -->',
|
|
152
|
-
noCommentReason: 'no-retro-comment',
|
|
153
|
-
parseFindings: () => [],
|
|
154
156
|
buildContentMarker,
|
|
155
157
|
buildMatchTokens,
|
|
156
158
|
crossRepoCommentAttrs: { graduator: 'retro-proposals' },
|
|
@@ -210,8 +212,8 @@ function toFinding(item, source, index) {
|
|
|
210
212
|
*
|
|
211
213
|
* @param {object} opts
|
|
212
214
|
* @param {number} opts.epicId
|
|
213
|
-
* @param {object} opts.provider — ticketing provider (
|
|
214
|
-
*
|
|
215
|
+
* @param {object} opts.provider — ticketing provider (`postComment`, for the
|
|
216
|
+
* cross-repo-deferred persistence).
|
|
215
217
|
* @param {object} [opts.config] — resolved agentrc.
|
|
216
218
|
* @param {{owner: string, repo: string}} opts.currentRepo — the repo the
|
|
217
219
|
* retro is running inside (the consumer's own repo); the cross-repo guard's
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/findings/provenance-field.js — the per-Story `provenance` field.
|
|
3
|
+
*
|
|
4
|
+
* An audit-seeded plan carries dedup identities forward so the next sweep
|
|
5
|
+
* recognises what it already planned. The optional top-level `provenance`
|
|
6
|
+
* field on a `stories.json` entry says **which of them that Story owns**:
|
|
7
|
+
*
|
|
8
|
+
* ```jsonc
|
|
9
|
+
* { "fingerprints": ["<40-char sha1>"], "semanticKeys": ["architecture␟lib/a.js"] }
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* Two callers, deliberately split from
|
|
13
|
+
* [`route-finding.js`](route-finding.js): the ticket validator shape-checks
|
|
14
|
+
* the authored field, and plan-persist's assembly renders the owned identities
|
|
15
|
+
* into the footer source it stamps. Neither is dedup *routing*, which is what
|
|
16
|
+
* `route-finding.js` is for — this module reads its identity vocabulary
|
|
17
|
+
* (`SHA1_RE`, `SEMANTIC_KEY_RE`, and the two footer renderers) from there so
|
|
18
|
+
* there is exactly one definition of what a fingerprint or a semantic key
|
|
19
|
+
* looks like.
|
|
20
|
+
*
|
|
21
|
+
* @module lib/findings/provenance-field
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
fingerprintFooter,
|
|
26
|
+
SEMANTIC_KEY_RE,
|
|
27
|
+
SHA1_RE,
|
|
28
|
+
semanticKeyFooter,
|
|
29
|
+
} from './route-finding.js';
|
|
30
|
+
|
|
31
|
+
/** Human-readable rendering of the `provenance` field's two lists. */
|
|
32
|
+
const PROVENANCE_SHAPE = 'fingerprints[] / semanticKeys[]';
|
|
33
|
+
|
|
34
|
+
/** What each `provenance` list accepts, and how to say so when it does not. */
|
|
35
|
+
const PROVENANCE_FIELDS = Object.freeze({
|
|
36
|
+
fingerprints: { pattern: SHA1_RE, expected: 'a 40-char sha1 hex string' },
|
|
37
|
+
semanticKeys: {
|
|
38
|
+
pattern: SEMANTIC_KEY_RE,
|
|
39
|
+
expected: 'a non-empty key carrying no comma or ">"',
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Validate one authored `provenance` list into its normalized form.
|
|
45
|
+
*
|
|
46
|
+
* @param {unknown} list
|
|
47
|
+
* @param {{ where: string, field: string, pattern: RegExp, expected: string }} spec
|
|
48
|
+
* @returns {string[]} Trimmed, de-duplicated, first-seen order.
|
|
49
|
+
*/
|
|
50
|
+
function normalizeList(list, { where, field, pattern, expected }) {
|
|
51
|
+
if (list === null || list === undefined) return [];
|
|
52
|
+
if (!Array.isArray(list)) {
|
|
53
|
+
throw new Error(`${where}: ${field} must be an array of strings`);
|
|
54
|
+
}
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const entry of list) {
|
|
57
|
+
const value = typeof entry === 'string' ? entry.trim() : '';
|
|
58
|
+
if (!pattern.test(value)) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`${where}: ${field} entry ${JSON.stringify(entry)} is not ${expected}`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (!out.includes(value)) out.push(value);
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Normalize the optional per-Story `provenance` field a plan may author —
|
|
70
|
+
* the identities of the findings **that Story owns**.
|
|
71
|
+
*
|
|
72
|
+
* Absence is meaningful and must stay cheap: `undefined` / `null` returns
|
|
73
|
+
* `null`, which is the caller's signal to fall back to the whole-seed union
|
|
74
|
+
* carry. That fallback is not vestigial — leaving the authoring agent to
|
|
75
|
+
* hand-carry provenance out of the seed's HTML comments was measured to fail,
|
|
76
|
+
* and the mechanical union is what closed it. Attribution is **additive**: a
|
|
77
|
+
* plan that attributes gets exact stamping, a plan that does not keeps recall.
|
|
78
|
+
*
|
|
79
|
+
* An empty object is therefore *not* the same as an absent field: it means
|
|
80
|
+
* "this Story owns nothing", and stamps nothing.
|
|
81
|
+
*
|
|
82
|
+
* Present-but-malformed is a hard error rather than a silent drop, because a
|
|
83
|
+
* dropped identity is invisible until the next sweep re-files work that was
|
|
84
|
+
* already planned.
|
|
85
|
+
*
|
|
86
|
+
* @param {unknown} raw
|
|
87
|
+
* @param {string} [label] Identifier for the error message (a Story slug).
|
|
88
|
+
* @returns {{ fingerprints: string[], semanticKeys: string[] }|null}
|
|
89
|
+
* @throws {Error} On any shape the stamper cannot honour exactly.
|
|
90
|
+
*/
|
|
91
|
+
export function normalizeOwnedProvenance(raw, label = 'story') {
|
|
92
|
+
if (raw === undefined || raw === null) return null;
|
|
93
|
+
const where = `provenance on "${label}"`;
|
|
94
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
95
|
+
throw new Error(`${where} must be an object of ${PROVENANCE_SHAPE}`);
|
|
96
|
+
}
|
|
97
|
+
const out = { fingerprints: [], semanticKeys: [] };
|
|
98
|
+
for (const [field, list] of Object.entries(raw)) {
|
|
99
|
+
const spec = PROVENANCE_FIELDS[field];
|
|
100
|
+
if (!spec) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`${where} carries an unknown field: ${field} — only ${PROVENANCE_SHAPE} are stamped`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
out[field] = normalizeList(list, { where, field, ...spec });
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Render the provenance **source document** for a set of owned identities, in
|
|
112
|
+
* the same footer vocabulary `carryProvenanceFooters` harvests from an audit
|
|
113
|
+
* seed. That reuse is the point: attribution changes *which* identities reach
|
|
114
|
+
* a Story body, never how they are stamped, so the carry stays additive,
|
|
115
|
+
* union-preserving and idempotent for an attributed plan exactly as it is for
|
|
116
|
+
* an un-attributed one.
|
|
117
|
+
*
|
|
118
|
+
* An empty (or absent) set renders the empty string, which the carry treats as
|
|
119
|
+
* nothing-to-do — so a Story that owns no findings is stamped with none rather
|
|
120
|
+
* than inheriting its siblings'.
|
|
121
|
+
*
|
|
122
|
+
* Expects the normalized shape {@link normalizeOwnedProvenance} returns; the
|
|
123
|
+
* validator runs first on every production path.
|
|
124
|
+
*
|
|
125
|
+
* @param {{ fingerprints?: string[], semanticKeys?: string[] }|null} [provenance]
|
|
126
|
+
* @returns {string}
|
|
127
|
+
*/
|
|
128
|
+
export function ownedProvenanceSource(provenance) {
|
|
129
|
+
const shas = provenance?.fingerprints ?? [];
|
|
130
|
+
const keys = provenance?.semanticKeys ?? [];
|
|
131
|
+
const parts = [];
|
|
132
|
+
if (shas.length > 0) parts.push(fingerprintFooter(shas));
|
|
133
|
+
if (keys.length > 0) parts.push(semanticKeyFooter(keys));
|
|
134
|
+
return parts.join('\n');
|
|
135
|
+
}
|