mandrel 2.30.0 → 2.32.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 +4 -4
- package/.agents/docs/agentrc-reference.json +61 -57
- package/.agents/docs/configuration.md +274 -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/ci-remediation.md +44 -1
- 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 +0 -1
- package/.agents/schemas/agentrc.schema.json +1939 -1400
- 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 +7 -29
- package/.agents/scripts/apply-quality-bootstrap.js +27 -34
- 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 +136 -0
- package/.agents/scripts/check-lifecycle-lint.js +36 -112
- 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 +0 -1
- 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/temp-paths.js +6 -5
- package/.agents/scripts/lib/config-settings-schema-delivery.js +237 -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/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/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-entry-sync.js +469 -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 +6 -0
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +0 -1
- package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
- package/.agents/scripts/lib/orchestration/resolve-stories.js +5 -15
- 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/push.js +17 -2
- 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 +65 -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-sizing.js +1 -1
- 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/story-body.js +12 -59
- 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/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/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 +0 -2
- 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/update-dead-exports-baseline.js +321 -0
- package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
- package/.agents/skills/skills.index.json +2 -12
- package/.agents/skills/stack/qa/playwright/SKILL.md +48 -0
- package/.agents/workflows/audit-documentation.md +5 -6
- package/.agents/workflows/audit-to-stories.md +2 -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 +2 -1
- package/.agents/workflows/helpers/deliver-story-reference.md +27 -16
- 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 +49 -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 +12 -3
- 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
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* knip-entry-sync.js — derive which top-level `.agents/scripts/*.js` CLIs are
|
|
3
|
+
* actually invoked, and diff that set against `knip.json`'s `entry` array.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists (Story #5001 → the #5012 near-miss):
|
|
6
|
+
*
|
|
7
|
+
* #5001 replaced the blanket `.agents/scripts/*.js!` entry glob with an
|
|
8
|
+
* explicit list, precisely so that a top-level CLI **nothing invokes** stops
|
|
9
|
+
* being declared live by the glob and surfaces as dead. It simultaneously
|
|
10
|
+
* promoted knip's `files` rule to `error`, so whole-file death now produces
|
|
11
|
+
* `{ file, symbol: '*' }` baseline rows.
|
|
12
|
+
*
|
|
13
|
+
* That pairing has a silent failure mode. A CLI added *after* #5001 is not in
|
|
14
|
+
* the hand-written list, so knip reads it as unreachable and emits a whole-file
|
|
15
|
+
* row for it — plus transitive rows for every lib module only that CLI imports.
|
|
16
|
+
* During #5012's delivery this produced 5 false rows (`check-baseline-scope.js`,
|
|
17
|
+
* `prune-baseline-orphans.js`, and their three `lib/baselines/*` modules). The
|
|
18
|
+
* natural remedy — accept the ratchet's diff — would have permanently recorded
|
|
19
|
+
* live, working operator CLIs as expected-dead, blinding the ratchet to their
|
|
20
|
+
* real death later. It was caught only because a base-sync conflict forced a
|
|
21
|
+
* manual read of the diff.
|
|
22
|
+
*
|
|
23
|
+
* The fix is to stop depending on a human remembering to edit `knip.json`.
|
|
24
|
+
* #5001's own wording already names the derivation rule — entries are derived
|
|
25
|
+
* "from what package.json scripts, husky hooks, .github/workflows, and the
|
|
26
|
+
* .agents workflow/skill markdown actually invoke". This module mechanizes
|
|
27
|
+
* exactly that rule and asserts it in both directions:
|
|
28
|
+
*
|
|
29
|
+
* missing — invoked by a real caller but absent from `entry`. This is the
|
|
30
|
+
* #5012 bug: knip will call it dead and the ratchet will offer to record
|
|
31
|
+
* the lie.
|
|
32
|
+
* stale — listed in `entry` but invoked by nothing. This is the blind spot
|
|
33
|
+
* #5001 closed: a declared entry point is immortal, so its death can never
|
|
34
|
+
* surface.
|
|
35
|
+
*
|
|
36
|
+
* Deliberately NOT a filesystem-derived list. Restoring `.agents/scripts/*.js!`
|
|
37
|
+
* — or regenerating the array from `readdirSync` — would make every file live
|
|
38
|
+
* by construction and undo #5001's AC-2. Liveness here means *invoked*, not
|
|
39
|
+
* *present*.
|
|
40
|
+
*
|
|
41
|
+
* Deliberately NOT conferred by documentation prose. `.agents/docs/**` and
|
|
42
|
+
* `docs/**` describe operator-run commands; a paragraph naming a CLI is not a
|
|
43
|
+
* caller. Three CLIs are named only in prose today
|
|
44
|
+
* (`check-baseline-drift.js`, `post-structured-comment.js`,
|
|
45
|
+
* `provision-git-hooks.js`) and #5001 recorded all of them as whole-file dead
|
|
46
|
+
* rows on purpose. Counting prose would silently resurrect them.
|
|
47
|
+
*
|
|
48
|
+
* A CLI that is only *imported* by another script (`cleanup-repo-test-temp.js`,
|
|
49
|
+
* imported by `run-tests.js` and `run-coverage.js`) is correctly neither: it is
|
|
50
|
+
* reachable through the module graph from a real entry, so knip already sees it
|
|
51
|
+
* and it must not be declared an entry point of its own.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
import fs from 'node:fs';
|
|
55
|
+
import path from 'node:path';
|
|
56
|
+
|
|
57
|
+
// Only `resolveEntrySync` and `renderEntrySyncReport` are public — they are
|
|
58
|
+
// what `check-knip-entries.js` calls. Everything else is module-private and
|
|
59
|
+
// reached by the suite through the `__testing` bundle at the foot of this file,
|
|
60
|
+
// the same seam `source-classifier.js` and five sibling modules use. Exporting
|
|
61
|
+
// the internals individually would put seven test-only symbols on the
|
|
62
|
+
// production dead-export baseline and make private helpers look like API.
|
|
63
|
+
|
|
64
|
+
/** Top-level CLI directory, repo-relative. */
|
|
65
|
+
const SCRIPTS_DIR = path.join('.agents', 'scripts');
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Executable surfaces — a mention of a CLI here is an instruction to run it.
|
|
69
|
+
*
|
|
70
|
+
* `dirs` entries are walked recursively and filtered by `test`. `files` are
|
|
71
|
+
* read directly. Documentation trees are absent by design (see module header).
|
|
72
|
+
*
|
|
73
|
+
* @type {ReadonlyArray<{ kind: 'file' | 'dir', at: string, test?: RegExp }>}
|
|
74
|
+
*/
|
|
75
|
+
const INVOCATION_SURFACES = Object.freeze([
|
|
76
|
+
{ kind: 'file', at: 'package.json' },
|
|
77
|
+
{ kind: 'dir', at: '.husky', test: /^(?!.*[/\\]_[/\\]).*$/ },
|
|
78
|
+
{ kind: 'dir', at: path.join('.github', 'workflows'), test: /\.ya?ml$/ },
|
|
79
|
+
{ kind: 'dir', at: path.join('.agents', 'workflows'), test: /\.md$/ },
|
|
80
|
+
{ kind: 'dir', at: path.join('.agents', 'skills'), test: /\.md$/ },
|
|
81
|
+
{ kind: 'dir', at: path.join('.agents', 'agents'), test: /\.md$/ },
|
|
82
|
+
{ kind: 'dir', at: path.join('.agents', 'rules'), test: /\.md$/ },
|
|
83
|
+
{ kind: 'dir', at: SCRIPTS_DIR, test: /\.js$/ },
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Strip line and block comments from JavaScript source, preserving string and
|
|
88
|
+
* template literals so a `'https://…'` or a `` `${x}//y` `` is not mangled.
|
|
89
|
+
*
|
|
90
|
+
* Comments are stripped before scanning `.js` surfaces so a JSDoc paragraph
|
|
91
|
+
* that merely *names* a CLI ("superseded by `node .agents/scripts/foo.js`")
|
|
92
|
+
* cannot confer liveness on it. Several such mentions exist today; every one
|
|
93
|
+
* of them is prose about a script's internals, not a call.
|
|
94
|
+
*
|
|
95
|
+
* Replaces comment bodies with equivalent whitespace rather than deleting
|
|
96
|
+
* them, so byte offsets and line numbers survive for any future caller that
|
|
97
|
+
* wants to report a position.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} source
|
|
100
|
+
* @returns {string}
|
|
101
|
+
*/
|
|
102
|
+
function stripJsComments(source) {
|
|
103
|
+
const text = String(source ?? '');
|
|
104
|
+
let out = '';
|
|
105
|
+
let i = 0;
|
|
106
|
+
const blank = (s) => s.replace(/[^\n]/g, ' ');
|
|
107
|
+
while (i < text.length) {
|
|
108
|
+
const two = text.slice(i, i + 2);
|
|
109
|
+
if (two === '//') {
|
|
110
|
+
const end = text.indexOf('\n', i);
|
|
111
|
+
const stop = end === -1 ? text.length : end;
|
|
112
|
+
out += blank(text.slice(i, stop));
|
|
113
|
+
i = stop;
|
|
114
|
+
} else if (two === '/*') {
|
|
115
|
+
const end = text.indexOf('*/', i + 2);
|
|
116
|
+
const stop = end === -1 ? text.length : end + 2;
|
|
117
|
+
out += blank(text.slice(i, stop));
|
|
118
|
+
i = stop;
|
|
119
|
+
} else {
|
|
120
|
+
const ch = text[i];
|
|
121
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
122
|
+
let j = i + 1;
|
|
123
|
+
while (j < text.length) {
|
|
124
|
+
if (text[j] === '\\') {
|
|
125
|
+
j += 2;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (text[j] === ch) break;
|
|
129
|
+
j += 1;
|
|
130
|
+
}
|
|
131
|
+
const stop = Math.min(j + 1, text.length);
|
|
132
|
+
out += text.slice(i, stop);
|
|
133
|
+
i = stop;
|
|
134
|
+
} else {
|
|
135
|
+
out += ch;
|
|
136
|
+
i += 1;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Recursively list files under `dir`, skipping `node_modules` and `.git`.
|
|
145
|
+
*
|
|
146
|
+
* @param {string} dir absolute path
|
|
147
|
+
* @param {typeof fs} fsImpl
|
|
148
|
+
* @param {string[]} [acc]
|
|
149
|
+
* @returns {string[]} absolute file paths
|
|
150
|
+
*/
|
|
151
|
+
function walk(dir, fsImpl, acc = []) {
|
|
152
|
+
let entries;
|
|
153
|
+
try {
|
|
154
|
+
entries = fsImpl.readdirSync(dir, { withFileTypes: true });
|
|
155
|
+
} catch {
|
|
156
|
+
return acc;
|
|
157
|
+
}
|
|
158
|
+
for (const entry of entries) {
|
|
159
|
+
const full = path.join(dir, entry.name);
|
|
160
|
+
if (entry.isDirectory()) {
|
|
161
|
+
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
162
|
+
walk(full, fsImpl, acc);
|
|
163
|
+
} else {
|
|
164
|
+
acc.push(full);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return acc;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Read every executable surface under `repoRoot`.
|
|
172
|
+
*
|
|
173
|
+
* `.js` surfaces arrive comment-stripped; every other surface arrives verbatim.
|
|
174
|
+
* Test files are excluded: a test importing or naming a CLI does not make it
|
|
175
|
+
* invoked, which is the same test-only-importer discount `--production` applies
|
|
176
|
+
* to the dead-exports ratchet itself.
|
|
177
|
+
*
|
|
178
|
+
* @param {{ repoRoot: string, fsImpl?: typeof fs }} opts
|
|
179
|
+
* @returns {Array<{ path: string, text: string }>} repo-relative path + content
|
|
180
|
+
*/
|
|
181
|
+
function collectInvocationSurfaces({ repoRoot, fsImpl = fs }) {
|
|
182
|
+
const surfaces = [];
|
|
183
|
+
const push = (absolute) => {
|
|
184
|
+
const rel = path.relative(repoRoot, absolute);
|
|
185
|
+
if (rel.includes('__tests__') || /\.test\.[cm]?js$/.test(rel)) return;
|
|
186
|
+
let text;
|
|
187
|
+
try {
|
|
188
|
+
text = fsImpl.readFileSync(absolute, 'utf8');
|
|
189
|
+
} catch {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
surfaces.push({
|
|
193
|
+
path: rel.split(path.sep).join('/'),
|
|
194
|
+
text: rel.endsWith('.js') ? stripJsComments(text) : text,
|
|
195
|
+
});
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
for (const surface of INVOCATION_SURFACES) {
|
|
199
|
+
const absolute = path.join(repoRoot, surface.at);
|
|
200
|
+
if (surface.kind === 'file') {
|
|
201
|
+
push(absolute);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
for (const file of walk(absolute, fsImpl)) {
|
|
205
|
+
const rel = path.relative(repoRoot, file);
|
|
206
|
+
if (surface.test && !surface.test.test(rel)) continue;
|
|
207
|
+
push(file);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return surfaces;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Build the two recognizers for one CLI basename.
|
|
215
|
+
*
|
|
216
|
+
* `pathLiteral` — the CLI named by its repo path (`.agents/scripts/foo.js`),
|
|
217
|
+
* in either separator style. This is how package.json scripts, husky hooks,
|
|
218
|
+
* workflow `run:` blocks and workflow markdown spell an invocation.
|
|
219
|
+
*
|
|
220
|
+
* `joinedSpawn` — the CLI named by bare basename as the tail of a
|
|
221
|
+
* `path.join(…, 'scripts', 'foo.js')` construction, which is how
|
|
222
|
+
* `lib/bootstrap/project-bootstrap.js` spawns `check-windows-git-perf.js`
|
|
223
|
+
* into a consumer checkout. Requiring the `'scripts'` sibling argument is
|
|
224
|
+
* what separates a real spawn from the several *data* inventories that list
|
|
225
|
+
* the same basenames as plain array elements (`source-classifier.js`'s
|
|
226
|
+
* `FRAMEWORK_SCRIPT_BASENAMES`, `check-lifecycle-lint.js`'s allowlist) —
|
|
227
|
+
* those are catalogues of names, not calls.
|
|
228
|
+
*
|
|
229
|
+
* @param {string} cli basename, e.g. `check-dead-exports.js`
|
|
230
|
+
* @returns {{ pathLiteral: RegExp, joinedSpawn: RegExp }}
|
|
231
|
+
*/
|
|
232
|
+
function buildInvocationPatterns(cli) {
|
|
233
|
+
const escaped = cli.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
234
|
+
return {
|
|
235
|
+
pathLiteral: new RegExp(`\\.agents[/\\\\]scripts[/\\\\]${escaped}`),
|
|
236
|
+
joinedSpawn: new RegExp(
|
|
237
|
+
`['"\`]scripts['"\`]\\s*,\\s*['"\`]${escaped}['"\`]`,
|
|
238
|
+
),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* List the top-level CLI basenames actually present in `.agents/scripts/`.
|
|
244
|
+
*
|
|
245
|
+
* @param {{ repoRoot: string, fsImpl?: typeof fs }} opts
|
|
246
|
+
* @returns {string[]} sorted basenames
|
|
247
|
+
*/
|
|
248
|
+
function listTopLevelClis({ repoRoot, fsImpl = fs }) {
|
|
249
|
+
let entries;
|
|
250
|
+
try {
|
|
251
|
+
entries = fsImpl.readdirSync(path.join(repoRoot, SCRIPTS_DIR), {
|
|
252
|
+
withFileTypes: true,
|
|
253
|
+
});
|
|
254
|
+
} catch {
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
return entries
|
|
258
|
+
.filter((e) => e.isFile() && e.name.endsWith('.js'))
|
|
259
|
+
.map((e) => e.name)
|
|
260
|
+
.sort();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Read the top-level `.agents/scripts/*.js` basenames declared in `knip.json`'s
|
|
265
|
+
* `entry` array. Glob-bearing patterns are ignored — this reads the explicit
|
|
266
|
+
* enumeration only, which is the surface #5001 made authoritative.
|
|
267
|
+
*
|
|
268
|
+
* The trailing `!` is knip's production-mode marker and is load-bearing, not
|
|
269
|
+
* decoration: in a `--production` run knip keeps only the suffixed entries and
|
|
270
|
+
* *negates* the rest (`WorkspaceWorker`'s `hasProductionSuffix` /
|
|
271
|
+
* `hasNoProductionSuffix` split). So an entry written without it leaves the CLI
|
|
272
|
+
* unreachable in exactly the pass that emits whole-file rows — the #5012 shape
|
|
273
|
+
* this gate exists to catch. Reading it as satisfied would point the operator
|
|
274
|
+
* away from the cause, so unsuffixed entries are reported as their own
|
|
275
|
+
* divergence rather than silently normalized.
|
|
276
|
+
*
|
|
277
|
+
* @param {{ repoRoot: string, fsImpl?: typeof fs }} opts
|
|
278
|
+
* @returns {{ entries: string[], unsuffixed: string[], error: string | null }}
|
|
279
|
+
*/
|
|
280
|
+
function readKnipEntries({ repoRoot, fsImpl = fs }) {
|
|
281
|
+
const configPath = path.join(repoRoot, 'knip.json');
|
|
282
|
+
let parsed;
|
|
283
|
+
try {
|
|
284
|
+
parsed = JSON.parse(fsImpl.readFileSync(configPath, 'utf8'));
|
|
285
|
+
} catch (error) {
|
|
286
|
+
return {
|
|
287
|
+
entries: [],
|
|
288
|
+
unsuffixed: [],
|
|
289
|
+
error: `cannot read knip.json: ${error.message}`,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
if (!Array.isArray(parsed?.entry)) {
|
|
293
|
+
return {
|
|
294
|
+
entries: [],
|
|
295
|
+
unsuffixed: [],
|
|
296
|
+
error: 'knip.json has no "entry" array',
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const prefix = '.agents/scripts/';
|
|
300
|
+
const declared = parsed.entry
|
|
301
|
+
.filter((e) => typeof e === 'string' && e.startsWith(prefix))
|
|
302
|
+
.filter((e) => !e.includes('*'))
|
|
303
|
+
.map((e) => ({
|
|
304
|
+
cli: e.slice(prefix.length).replace(/!$/, ''),
|
|
305
|
+
suffixed: e.endsWith('!'),
|
|
306
|
+
}))
|
|
307
|
+
.filter((e) => !e.cli.includes('/'));
|
|
308
|
+
|
|
309
|
+
const entries = declared.map((e) => e.cli);
|
|
310
|
+
const unsuffixed = declared.filter((e) => !e.suffixed).map((e) => e.cli);
|
|
311
|
+
return {
|
|
312
|
+
entries: [...new Set(entries)].sort(),
|
|
313
|
+
unsuffixed: [...new Set(unsuffixed)].sort(),
|
|
314
|
+
error: null,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Resolve the full entry-sync report for a repository.
|
|
320
|
+
*
|
|
321
|
+
* @param {{ repoRoot: string, fsImpl?: typeof fs }} opts
|
|
322
|
+
* @returns {{
|
|
323
|
+
* error: string | null,
|
|
324
|
+
* clis: string[],
|
|
325
|
+
* declared: string[],
|
|
326
|
+
* missing: Array<{ cli: string, invokers: string[] }>,
|
|
327
|
+
* stale: string[],
|
|
328
|
+
* phantom: string[],
|
|
329
|
+
* unsuffixed: string[],
|
|
330
|
+
* }}
|
|
331
|
+
* `missing` — invoked but not declared (the #5012 bug).
|
|
332
|
+
* `stale` — declared but invoked by nothing.
|
|
333
|
+
* `phantom` — declared but no such file on disk (a rename left the list behind).
|
|
334
|
+
* `unsuffixed` — declared without the `!` production marker, which knip's
|
|
335
|
+
* production pass negates rather than honours (the #5012 bug wearing a
|
|
336
|
+
* declared entry).
|
|
337
|
+
*/
|
|
338
|
+
export function resolveEntrySync({ repoRoot, fsImpl = fs }) {
|
|
339
|
+
const {
|
|
340
|
+
entries: declared,
|
|
341
|
+
unsuffixed,
|
|
342
|
+
error,
|
|
343
|
+
} = readKnipEntries({ repoRoot, fsImpl });
|
|
344
|
+
const empty = {
|
|
345
|
+
clis: [],
|
|
346
|
+
declared,
|
|
347
|
+
missing: [],
|
|
348
|
+
stale: [],
|
|
349
|
+
phantom: [],
|
|
350
|
+
unsuffixed: [],
|
|
351
|
+
};
|
|
352
|
+
if (error) return { error, ...empty };
|
|
353
|
+
|
|
354
|
+
const clis = listTopLevelClis({ repoRoot, fsImpl });
|
|
355
|
+
if (clis.length === 0) {
|
|
356
|
+
return { error: `no CLIs found under ${SCRIPTS_DIR}`, ...empty };
|
|
357
|
+
}
|
|
358
|
+
const surfaces = collectInvocationSurfaces({ repoRoot, fsImpl });
|
|
359
|
+
const declaredSet = new Set(declared);
|
|
360
|
+
|
|
361
|
+
const missing = [];
|
|
362
|
+
const stale = [];
|
|
363
|
+
for (const cli of clis) {
|
|
364
|
+
const self = `${SCRIPTS_DIR.split(path.sep).join('/')}/${cli}`;
|
|
365
|
+
const { pathLiteral, joinedSpawn } = buildInvocationPatterns(cli);
|
|
366
|
+
const invokers = surfaces
|
|
367
|
+
.filter(
|
|
368
|
+
(s) =>
|
|
369
|
+
s.path !== self &&
|
|
370
|
+
(pathLiteral.test(s.text) ||
|
|
371
|
+
(s.path.endsWith('.js') && joinedSpawn.test(s.text))),
|
|
372
|
+
)
|
|
373
|
+
.map((s) => s.path);
|
|
374
|
+
const isDeclared = declaredSet.has(cli);
|
|
375
|
+
if (invokers.length > 0 && !isDeclared) missing.push({ cli, invokers });
|
|
376
|
+
if (invokers.length === 0 && isDeclared) stale.push(cli);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const onDisk = new Set(clis);
|
|
380
|
+
const phantom = declared.filter((cli) => !onDisk.has(cli));
|
|
381
|
+
|
|
382
|
+
return { error: null, clis, declared, missing, stale, phantom, unsuffixed };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Total divergences across every direction. Single source of the gate's
|
|
387
|
+
* pass/fail arithmetic so a newly added direction cannot be counted in the
|
|
388
|
+
* report but missed by the exit code.
|
|
389
|
+
*
|
|
390
|
+
* @param {ReturnType<typeof resolveEntrySync>} report
|
|
391
|
+
* @returns {number}
|
|
392
|
+
*/
|
|
393
|
+
export function countDivergences(report) {
|
|
394
|
+
return (
|
|
395
|
+
report.missing.length +
|
|
396
|
+
report.stale.length +
|
|
397
|
+
report.phantom.length +
|
|
398
|
+
report.unsuffixed.length
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Render the human-readable report. Each divergence names its own remedy —
|
|
404
|
+
* the whole point of the gate is that the *wrong* remedy (accepting the
|
|
405
|
+
* dead-exports ratchet's diff) is the tempting one.
|
|
406
|
+
*
|
|
407
|
+
* @param {ReturnType<typeof resolveEntrySync>} report
|
|
408
|
+
* @returns {string}
|
|
409
|
+
*/
|
|
410
|
+
export function renderEntrySyncReport(report) {
|
|
411
|
+
const lines = [];
|
|
412
|
+
for (const { cli, invokers } of report.missing) {
|
|
413
|
+
lines.push(
|
|
414
|
+
`+ ${cli} — invoked by ${invokers.slice(0, 3).join(', ')}${
|
|
415
|
+
invokers.length > 3 ? ` (+${invokers.length - 3} more)` : ''
|
|
416
|
+
}`,
|
|
417
|
+
);
|
|
418
|
+
lines.push(
|
|
419
|
+
` add ".agents/scripts/${cli}!" to knip.json "entry" — until you do, knip`,
|
|
420
|
+
);
|
|
421
|
+
lines.push(
|
|
422
|
+
' reads it as unreachable and check-dead-exports will offer a false whole-file row.',
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
for (const cli of report.stale) {
|
|
426
|
+
lines.push(
|
|
427
|
+
`- ${cli} — declared in knip.json "entry" but nothing invokes it`,
|
|
428
|
+
);
|
|
429
|
+
lines.push(
|
|
430
|
+
' remove the entry so its death can surface, or wire up the caller.',
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
for (const cli of report.phantom) {
|
|
434
|
+
lines.push(`? ${cli} — declared in knip.json "entry" but not on disk`);
|
|
435
|
+
lines.push(' drop the stale path (the file was renamed or removed).');
|
|
436
|
+
}
|
|
437
|
+
for (const cli of report.unsuffixed) {
|
|
438
|
+
lines.push(`! ${cli} — declared in knip.json "entry" without a "!" suffix`);
|
|
439
|
+
lines.push(
|
|
440
|
+
` write it as ".agents/scripts/${cli}!" — the production pass negates an`,
|
|
441
|
+
);
|
|
442
|
+
lines.push(
|
|
443
|
+
' unsuffixed entry, so knip still reads the CLI as unreachable.',
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
const total = countDivergences(report);
|
|
447
|
+
lines.push(
|
|
448
|
+
`[knip-entries] clis=${report.clis.length} declared=${report.declared.length} ` +
|
|
449
|
+
`missing=${report.missing.length} stale=${report.stale.length} ` +
|
|
450
|
+
`phantom=${report.phantom.length} unsuffixed=${report.unsuffixed.length} ` +
|
|
451
|
+
`${total > 0 ? '(gate fail)' : '(ok)'}`,
|
|
452
|
+
);
|
|
453
|
+
return lines.join('\n');
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Test-only seam. Not API: these helpers are exercised directly by
|
|
458
|
+
* `tests/check-knip-entries.test.js` so the recognizers can be pinned
|
|
459
|
+
* independently of a full fixture tree.
|
|
460
|
+
*/
|
|
461
|
+
export const __testing = Object.freeze({
|
|
462
|
+
buildInvocationPatterns,
|
|
463
|
+
collectInvocationSurfaces,
|
|
464
|
+
INVOCATION_SURFACES,
|
|
465
|
+
listTopLevelClis,
|
|
466
|
+
readKnipEntries,
|
|
467
|
+
SCRIPTS_DIR,
|
|
468
|
+
stripJsComments,
|
|
469
|
+
});
|
|
@@ -74,7 +74,7 @@ export const MAX_LEDGER_BYTES = 1024 * 1024;
|
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
76
|
* Resolve the ledger path for an Epic (or the standalone stream when
|
|
77
|
-
* `epicId` is `null` — the
|
|
77
|
+
* `epicId` is `null` — the v2 Epic-less plan/healthcheck case).
|
|
78
78
|
*
|
|
79
79
|
* @param {number|null} epicId
|
|
80
80
|
* @param {object} [config] Resolved config (threads `project.paths.tempRoot`).
|
|
@@ -83,6 +83,16 @@ export const RUNTIME_FRICTION_CATEGORIES = Object.freeze({
|
|
|
83
83
|
* ceilings recalibratable from recorded evidence.
|
|
84
84
|
*/
|
|
85
85
|
LIGHT_SCOPE_REJECTED: 'light-scope-rejected',
|
|
86
|
+
/**
|
|
87
|
+
* An operator overrode a Story-scope code-review critical blocker with
|
|
88
|
+
* `--override-review-block <reason>`. Deliberately its own
|
|
89
|
+
* bucket: this is the one signal that says a gate's verdict was rejected by
|
|
90
|
+
* a human who then shipped anyway, so a rising `occurrences` count is direct
|
|
91
|
+
* evidence the gate is miscalibrated rather than the code being bad. It is
|
|
92
|
+
* *not* `TOOL_DEGRADED` — the tool ran fine and produced a verdict; what
|
|
93
|
+
* failed was the verdict's usefulness.
|
|
94
|
+
*/
|
|
95
|
+
REVIEW_BLOCK_OVERRIDDEN: 'review-block-overridden',
|
|
86
96
|
});
|
|
87
97
|
|
|
88
98
|
/** Cap on free-form reason text copied into a signal's `details`. */
|
|
@@ -17,22 +17,20 @@
|
|
|
17
17
|
* document (no hand-rolled drift). `strict: false` matches the repo's
|
|
18
18
|
* other draft-07 validators (see `tests/schemas/signal-schemas.test.js`).
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
* (`temp/run-<eid>/signal-rejects.json`)
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
20
|
+
* Story #5003 deleted the persisted per-Epic reject tally
|
|
21
|
+
* (`temp/run-<eid>/signal-rejects.json`) and its `readSignalRejectCount`
|
|
22
|
+
* reader: the tally was keyed on a positive `epicId`, and v2 Stories are
|
|
23
|
+
* standalone, so nothing ever wrote a row and nothing could ever read one.
|
|
24
|
+
* Write-time validation itself is untouched.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import { readFileSync } from 'node:fs';
|
|
28
|
-
import fs from 'node:fs/promises';
|
|
29
28
|
import path from 'node:path';
|
|
30
29
|
import { fileURLToPath } from 'node:url';
|
|
31
30
|
|
|
32
31
|
import Ajv from 'ajv';
|
|
33
32
|
import addFormats from 'ajv-formats';
|
|
34
33
|
|
|
35
|
-
import { runArtifactPath } from '../config/temp-paths.js';
|
|
36
34
|
import { Logger } from '../Logger.js';
|
|
37
35
|
|
|
38
36
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -45,8 +43,6 @@ const SCHEMA_PATH = path.resolve(
|
|
|
45
43
|
'signal-event.schema.json',
|
|
46
44
|
);
|
|
47
45
|
|
|
48
|
-
const REJECT_TALLY_BASENAME = 'signal-rejects.json';
|
|
49
|
-
|
|
50
46
|
/**
|
|
51
47
|
* Compile the signal-event schema once. Returns `null` when the schema
|
|
52
48
|
* cannot be read or compiled — the caller then treats every record as
|
|
@@ -126,79 +122,3 @@ export function validateSignal(record) {
|
|
|
126
122
|
const message = _validate.errors?.[0]?.message ?? 'schema validation failed';
|
|
127
123
|
return { valid: false, violatingField: field, message };
|
|
128
124
|
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Increment the per-Epic reject tally persisted under the Epic temp tree.
|
|
132
|
-
* Best-effort and never throws: a missing/corrupt tally file resets to a
|
|
133
|
-
* count of 1. When `epicId` is not a positive integer (e.g. a standalone
|
|
134
|
-
* friction record with `epicId: null`) the tally write is skipped — there
|
|
135
|
-
* is no Epic temp tree to anchor it to.
|
|
136
|
-
*
|
|
137
|
-
* @param {{ epicId: number|null|undefined, config?: object, field?: string|null }} args
|
|
138
|
-
* @returns {Promise<number|null>} the new count, or null when skipped/failed.
|
|
139
|
-
*/
|
|
140
|
-
export async function recordSignalReject({ epicId, config, field }) {
|
|
141
|
-
if (!Number.isInteger(epicId) || epicId <= 0) return null;
|
|
142
|
-
let target;
|
|
143
|
-
try {
|
|
144
|
-
target = runArtifactPath(epicId, REJECT_TALLY_BASENAME, config);
|
|
145
|
-
} catch {
|
|
146
|
-
return null;
|
|
147
|
-
}
|
|
148
|
-
try {
|
|
149
|
-
let prior = { count: 0, lastField: null };
|
|
150
|
-
try {
|
|
151
|
-
const raw = await fs.readFile(target, 'utf8');
|
|
152
|
-
const parsed = JSON.parse(raw);
|
|
153
|
-
if (
|
|
154
|
-
parsed &&
|
|
155
|
-
typeof parsed === 'object' &&
|
|
156
|
-
Number.isFinite(parsed.count)
|
|
157
|
-
) {
|
|
158
|
-
prior = { count: parsed.count, lastField: parsed.lastField ?? null };
|
|
159
|
-
}
|
|
160
|
-
} catch {
|
|
161
|
-
// Missing or corrupt file → start a fresh tally.
|
|
162
|
-
}
|
|
163
|
-
const next = {
|
|
164
|
-
count: prior.count + 1,
|
|
165
|
-
lastField: field ?? prior.lastField ?? null,
|
|
166
|
-
updatedAt: new Date().toISOString(),
|
|
167
|
-
};
|
|
168
|
-
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
169
|
-
await fs.writeFile(target, `${JSON.stringify(next)}\n`, 'utf8');
|
|
170
|
-
return next.count;
|
|
171
|
-
} catch (err) {
|
|
172
|
-
Logger.warn(
|
|
173
|
-
`signal-validator: failed to persist reject tally for epic-${epicId}: ${
|
|
174
|
-
err instanceof Error ? err.message : String(err)
|
|
175
|
-
}`,
|
|
176
|
-
);
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Read the current per-Epic reject count. Cross-process readable
|
|
183
|
-
* counterpart to {@link recordSignalReject}. Returns 0 when the tally
|
|
184
|
-
* file is absent or unreadable.
|
|
185
|
-
*
|
|
186
|
-
* @param {{ epicId: number, config?: object }} args
|
|
187
|
-
* @returns {Promise<number>}
|
|
188
|
-
*/
|
|
189
|
-
export async function readSignalRejectCount({ epicId, config }) {
|
|
190
|
-
if (!Number.isInteger(epicId) || epicId <= 0) return 0;
|
|
191
|
-
let target;
|
|
192
|
-
try {
|
|
193
|
-
target = runArtifactPath(epicId, REJECT_TALLY_BASENAME, config);
|
|
194
|
-
} catch {
|
|
195
|
-
return 0;
|
|
196
|
-
}
|
|
197
|
-
try {
|
|
198
|
-
const raw = await fs.readFile(target, 'utf8');
|
|
199
|
-
const parsed = JSON.parse(raw);
|
|
200
|
-
return parsed && Number.isFinite(parsed.count) ? parsed.count : 0;
|
|
201
|
-
} catch {
|
|
202
|
-
return 0;
|
|
203
|
-
}
|
|
204
|
-
}
|