@event4u/agent-config 14.21.0 → 14.22.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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +198 -0
- package/CREDITS.md +37 -0
- package/dist/agent-src/commands/analyze/inbox.md +380 -5
- package/dist/agent-src/commands/analyze/repo.md +4 -2
- package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
- package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
- package/dist/agent-src/rules/token-efficiency.md +26 -3
- package/dist/agent-src/scripts/_cli_entry.ts +51 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
- package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
- package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
- package/dist/agent-src/scripts/stubs_due.ts +4 -18
- package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
- package/dist/agent-src/skills/code-intelligence/SKILL.md +25 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
- package/dist/agent-src/templates/agent-settings.md +0 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/_cli_entry.js +8 -0
- package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
- package/dist/cli-delegate/archival_sweep.js +8 -0
- package/dist/cli-delegate/archive_completed_roadmaps.js +494 -0
- package/dist/cli-delegate/check_update_banner.js +923 -0
- package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
- package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
- package/dist/cli-delegate/{chunk-643Z7FIC.js → chunk-BIXEDQPZ.js} +6 -0
- package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
- package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
- package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
- package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
- package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-TGIPZBCW.js} +1 -1
- package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
- package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
- package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
- package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
- package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
- package/dist/cli-delegate/cmd_conformance.js +2 -2
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +26 -2
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/cli-delegate/dashboard_mode.js +16 -0
- package/dist/cli-delegate/gate_budget.js +24 -0
- package/dist/cli-delegate/gate_execute.js +25 -0
- package/dist/cli-delegate/guarded_baseline.js +18 -0
- package/dist/cli-delegate/resume_probe.js +22 -0
- package/dist/cli-delegate/roadmap_gates.js +638 -0
- package/dist/cli-delegate/roadmap_set_step.js +173 -0
- package/dist/cli-delegate/stub_queue.js +47 -0
- package/dist/cli-delegate/stubs_due.js +38 -0
- package/dist/cli-delegate/ticket_bundles.js +59 -0
- package/dist/cli-delegate/update_roadmap_progress.js +60 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +8 -8
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +1193 -917
- package/dist/install/claudeLayerCarriage.js +237 -0
- package/dist/install/claudePathsPlan.js +79 -0
- package/dist/install/hostLayerFingerprint.js +1 -1
- package/dist/install/install.mjs +6 -0
- package/dist/install/partitionEligibility.js +231 -117
- package/dist/install/ruleLayerPartition.js +10 -7
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +39219 -38995
- package/dist/scripts/condense.js +32 -25
- package/docs/MIGRATION.md +49 -1
- package/docs/contracts/settings-classes.md +1 -1
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
- package/package.json +7 -4
- package/src/agent-src/scripts/_cli_entry.ts +51 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
- package/src/agent-src/scripts/roadmap_gates.ts +4 -20
- package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
- package/src/agent-src/scripts/stubs_due.ts +4 -18
- package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
- package/src/config/continuity-surface.json +509 -0
- package/src/config/estate-count-budget.json +1 -1
- package/src/config/gate-coverage.yml +29 -8
- package/src/config/gate-violation-baselines.json +8 -8
- package/src/config/gitignore-block.txt +19 -0
- package/src/config/host-injection-effect.json +73 -0
- package/src/config/packed-binary-manifest.json +33 -0
- package/src/config/preamble-payload-budget.json +112 -105
- package/src/config/publish-surface.json +4 -2
- package/src/install/claudeLayerCarriage.ts +273 -0
- package/src/install/claudePathsPlan.ts +76 -0
- package/src/install/hostLayerFingerprint.ts +1 -1
- package/src/install/partitionEligibility.ts +261 -127
- package/src/install/ruleLayerPartition.ts +19 -23
- package/src/scripts/_dispatch.bash +77 -27
- package/src/scripts/_lib/auto_dispatch.ts +2 -2
- package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
- package/src/scripts/_lib/continuity_surface.ts +296 -0
- package/src/scripts/_lib/corpus_manifest.ts +26 -9
- package/src/scripts/_lib/hook_settings.ts +1 -1
- package/src/scripts/_lib/injection_effect.ts +151 -0
- package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
- package/src/scripts/_lib/packed_binary_predicate.ts +225 -0
- package/src/scripts/_lib/pin_resolver.ts +13 -0
- package/src/scripts/_lib/regression_neighbourhood.ts +1 -1
- package/src/scripts/_lib/retired_status.ts +74 -0
- package/src/scripts/_lib/skill_catalogue.ts +156 -21
- package/src/scripts/_lib/subagent_capsule.ts +117 -8
- package/src/scripts/ai_council/api_transport.ts +5 -0
- package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
- package/src/scripts/ai_council/prompts.ts +9 -0
- package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
- package/src/scripts/capture_skill_catalogue.ts +24 -11
- package/src/scripts/check_candidate_lines.ts +499 -0
- package/src/scripts/check_continuity_surface.ts +475 -0
- package/src/scripts/check_dependency_floors.ts +13 -6
- package/src/scripts/check_estate_count.ts +40 -79
- package/src/scripts/check_pack_size.ts +262 -23
- package/src/scripts/check_roadmap_trackable.ts +19 -16
- package/src/scripts/check_rule_layer_partition.ts +54 -34
- package/src/scripts/check_single_delivery.ts +97 -31
- package/src/scripts/check_update_banner.ts +13 -0
- package/src/scripts/code_graph/build.ts +138 -15
- package/src/scripts/code_graph/cli.ts +76 -8
- package/src/scripts/code_graph/extract.ts +11 -0
- package/src/scripts/code_graph/grammar_probe.ts +43 -0
- package/src/scripts/code_graph/loader.ts +88 -14
- package/src/scripts/code_graph/query.ts +121 -11
- package/src/scripts/code_graph/resolution_tiers.ts +238 -0
- package/src/scripts/code_graph/sqlite_store.ts +247 -6
- package/src/scripts/code_graph/types.ts +66 -1
- package/src/scripts/code_graph/validate.ts +23 -0
- package/src/scripts/condense.ts +34 -22
- package/src/scripts/corpus_manifest.ts +5 -5
- package/src/scripts/council_cli.ts +11 -12
- package/src/scripts/count_intent_disagreement.ts +426 -0
- package/src/scripts/handoff_context_hook.ts +79 -0
- package/src/scripts/hook_effect_doctor.ts +82 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +64 -12
- package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
- package/src/scripts/hooks/code_graph_context_hook.ts +212 -0
- package/src/scripts/hooks/concern_registry.ts +4 -2
- package/src/scripts/hooks/host_semantics.ts +1 -1
- package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
- package/src/scripts/hooks/rule_inject_hook.ts +1 -1
- package/src/scripts/hooks/skill_route_hook.ts +11 -4
- package/src/scripts/inbox_source_census.ts +322 -0
- package/src/scripts/install-hooks.sh +82 -0
- package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
- package/src/scripts/lint_plan_risk_register.ts +13 -17
- package/src/scripts/mcp_server/suggest_skill.ts +72 -0
- package/src/scripts/mcp_server/tools.ts +2 -56
- package/src/scripts/media/lib/adapter-contract.md +7 -2
- package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
- package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
- package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
- package/src/scripts/rdp_candidates_delta.ts +276 -0
- package/src/scripts/rdp_quality_eval.ts +217 -19
- package/src/scripts/release_mix_taxonomy.json +3 -2
- package/src/scripts/report_host_injection_effect.ts +189 -0
- package/src/scripts/report_layer_overlap.ts +41 -0
- package/src/scripts/report_skill_menu_census.ts +341 -0
- package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
- package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
- package/src/vendor/grammars/README.md +61 -0
- package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
- package/src/scripts/_lib/carrier_status.ts +0 -118
- package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
- package/src/scripts/hooks/code_graph_nudge_hook.ts +0 -191
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* check_candidate_lines.ts — the SHAPE of the candidate-forms line, and the
|
|
4
|
+
* eval instrument that measures it. Not a shipped obligation on anyone.
|
|
5
|
+
*
|
|
6
|
+
* Steps 2.3, 2.5 and 2.6 of `road-to-candidate-moves-floor` define what a
|
|
7
|
+
* `Candidates:` line must look like; step 3.1 needs the three deterministic
|
|
8
|
+
* counts (line present at owed slots, `K0` present, axes pairwise distinct)
|
|
9
|
+
* before any rubric scoring runs. This file is both.
|
|
10
|
+
*
|
|
11
|
+
* WHY IT IS A SEPARATE SCRIPT, and why `lint_mandated_lines.ts` is untouched.
|
|
12
|
+
* The split AI council of 2026-09-07 blocked one specific thing: shipping a
|
|
13
|
+
* sixth line into `mandated-lines.md` § The five lines and giving
|
|
14
|
+
* `lint_mandated_lines` a third obligation. That is the "globally mandatory"
|
|
15
|
+
* step. It explicitly authorised the other half — `Candidates:` may be
|
|
16
|
+
* instructed to the model under test in the eval treatment arm, "where it can
|
|
17
|
+
* be measured without being a shipped obligation on anyone". A measurement
|
|
18
|
+
* instrument is that half. Adding the obligation to the existing linter would
|
|
19
|
+
* be the blocked half, so this checker lives beside it and nothing imports it
|
|
20
|
+
* into the shipped gate.
|
|
21
|
+
*
|
|
22
|
+
* THE GRAMMAR, one line, candidates separated by the house middle dot:
|
|
23
|
+
*
|
|
24
|
+
* Candidates: K0 keep the current shape · A extract a Notifier service
|
|
25
|
+
* [ownership boundary] · B pass a callback [call-site coupling] → A; three
|
|
26
|
+
* callers already build the payload, so the boundary exists already.
|
|
27
|
+
*
|
|
28
|
+
* `K0` is `keep the current form / change nothing` and is owed first. A forced
|
|
29
|
+
* form — exactly one admissible shape — carries the constraint instead of an
|
|
30
|
+
* invented alternative:
|
|
31
|
+
*
|
|
32
|
+
* Candidates: K0 keep the registration in `register()` [forced: the base
|
|
33
|
+
* class fixes the extension point] → K0; no other location is admissible.
|
|
34
|
+
*
|
|
35
|
+
* WHAT IT CANNOT CHECK. Not whether the candidates were generated before the
|
|
36
|
+
* choice. A line with an axis and a discriminator is reconstructable in ten
|
|
37
|
+
* seconds after deciding, and no textual check sees the order — the roadmap
|
|
38
|
+
* lists that as its own first risk, and `mandated-lines.md` § Honest scope
|
|
39
|
+
* says the same about the intent line. What IS decidable is that the axes are
|
|
40
|
+
* pairwise distinct, which kills the cosmetic set where three candidates
|
|
41
|
+
* differ only in where a helper lives.
|
|
42
|
+
*
|
|
43
|
+
* Exit: 0 clean · 1 usage/IO error · 2 findings.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import * as fs from 'node:fs';
|
|
47
|
+
import * as path from 'node:path';
|
|
48
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
49
|
+
|
|
50
|
+
import { contentWords, overlap } from './count_intent_disagreement.js';
|
|
51
|
+
import { stripFences, unwrapLines } from './lint_mandated_lines.js';
|
|
52
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
53
|
+
|
|
54
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
55
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
56
|
+
|
|
57
|
+
const SLOT_SEPARATOR = '·';
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* `Candidates: …` — one physical line, EMPHASIS-TOLERANT.
|
|
61
|
+
*
|
|
62
|
+
* The optional `**`/`__`/`*`/`_` runs are not cosmetic tolerance; they are the
|
|
63
|
+
* difference between a reading and an artefact. The first version copied the
|
|
64
|
+
* shipped `INTENT_RE` shape, which anchors the label at line start with no
|
|
65
|
+
* emphasis allowed. The 2026-09-08 treatment run then emitted its ONE
|
|
66
|
+
* compliant line as `**Candidates:** K0 …` — markdown bold, which every
|
|
67
|
+
* assistant reaches for on a labelled line — and the checker reported
|
|
68
|
+
* `Candidates line present: 0` over 32 transcripts. A detector that cannot see
|
|
69
|
+
* the thing it counts reproduces the defect it is measuring, and it does so
|
|
70
|
+
* silently, in the direction that confirms the null.
|
|
71
|
+
*
|
|
72
|
+
* The same gap exists in `lint_mandated_lines.ts`'s `INTENT_RE` and in the four
|
|
73
|
+
* other shipped labels. It is recorded as a finding rather than fixed here:
|
|
74
|
+
* that file is a shipped gate with 19 tests of its own, adjacent to the
|
|
75
|
+
* contract the 2026-09-07 council blocked, and widening its discrimination is
|
|
76
|
+
* its own change.
|
|
77
|
+
*
|
|
78
|
+
* CASE-SENSITIVE on the label. Widening with an `i` flag was tried on the
|
|
79
|
+
* sibling counter and took its population from 0 to 172, almost all of them
|
|
80
|
+
* lowercase `intent:` YAML keys in config — a line anchor turned into a prose
|
|
81
|
+
* detector. The instructed label is `Candidates:` and that is what the one
|
|
82
|
+
* real emission carried.
|
|
83
|
+
*/
|
|
84
|
+
export const CANDIDATES_RE =
|
|
85
|
+
/^[ \t]*(?:>[ \t]*)?(?:[-*+][ \t]+)?(?:\*\*|__|\*|_)?Candidates(?:\*\*|__|\*|_)?(?:[ \t]*\([^)]*\))?(?:\*\*|__|\*|_)?:(?:\*\*|__|\*|_)?[ \t]*(.+)$/gm;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Decision classes where a form choice actually exists — step 2.3.
|
|
89
|
+
*
|
|
90
|
+
* A FILE COUNT IS THE WRONG TRIGGER and that is the point of the list: a
|
|
91
|
+
* controller plus its test is two files and no architectural choice, while a
|
|
92
|
+
* rename touches four and offers none. These match the semantics instead.
|
|
93
|
+
*/
|
|
94
|
+
export const OWED_CLASS_TRIGGERS: readonly (readonly [string, RegExp])[] = [
|
|
95
|
+
// The `(?:\w+ ){0,3}` gaps are load-bearing: the first version required the
|
|
96
|
+
// noun to follow the article immediately, so "extracted a Notifier service"
|
|
97
|
+
// — the canonical case — did not fire at all.
|
|
98
|
+
['ownership-boundary', /\b(ownership boundary|new (?:\w+ ){0,2}(?:service|module|package|bounded context)|extract(?:ed|ing|s)? (?:a |an |the )?(?:\w+ ){0,3}(?:service|module|class|package))\b/i],
|
|
99
|
+
['new-abstraction', /\b(new (?:\w+ ){0,2}abstraction|introduce[sd]? (?:an |a |the )?(?:\w+ ){0,3}(?:interface|abstraction|base class|strategy|candidate)|abstract(?:ed|ing)? (?:out|over))\b/i],
|
|
100
|
+
['contract-change', /\b((?:contract|schema|DTO|signature|API response|payload)s?\b[^.]{0,40}\bchang|chang[^.]{0,40}\b(?:contract|schema|DTO|signature|payload)s?\b|migrat(?:e|ed|ion)\b[^.]{0,30}\bcolumn)\b/i],
|
|
101
|
+
['state-transition', /\b(state (?:transition|machine)|status transition|lifecycle state)\b/i],
|
|
102
|
+
['migration-rollout', /\b(migration|rollout|roll-out|backfill|expand-contract)\b/i],
|
|
103
|
+
['failure-semantics', /\b(failure semantics|error semantics|retry (?:policy|semantics)|timeout (?:policy|semantics)|new (?:exception|error) (?:type|class))\b/i],
|
|
104
|
+
['irreversible-action', /\b(irreversible|destructive|force[- ]push|drop (?:table|column)|truncate)\b/i],
|
|
105
|
+
['unresolved-root-cause', /\b(root cause (?:is |remains )?(?:unknown|unresolved|not (?:yet )?(?:found|established))|cause (?:is|remains) unclear)\b/i],
|
|
106
|
+
['maintainer-decision', /\b((?:roadmap|budget|ratchet|projection) (?:shape|scope|baseline)|re-?scope[sd]? the roadmap|lower(?:ed|ing)? the (?:baseline|ratchet))\b/i],
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Classes where the line is NOT owed — step 2.3's other half.
|
|
111
|
+
*
|
|
112
|
+
* These suppress ONLY when no owed trigger fired. The asymmetry is deliberate
|
|
113
|
+
* and is the safe direction: a rename that also changes a contract is a
|
|
114
|
+
* contract change, so an exemption may never overrule a positive owed match.
|
|
115
|
+
* Residual, stated rather than discovered later: a report that describes an
|
|
116
|
+
* abstraction change using only rename vocabulary owes nothing, and no lexical
|
|
117
|
+
* check fixes that.
|
|
118
|
+
*/
|
|
119
|
+
export const NOT_OWED_TRIGGERS: readonly (readonly [string, RegExp])[] = [
|
|
120
|
+
['typo', /\b(typo|misspell|spelling fix)\b/i],
|
|
121
|
+
['format', /\b(reformat|formatting|whitespace|prettier|lint fix|style-only)\b/i],
|
|
122
|
+
['pure-rename', /\b(pure(?:ly)? rename|rename[sd]? (?:the )?(?:variable|file|symbol|method|function)\b)/i],
|
|
123
|
+
['user-prescribed', /\b(the user (?:prescribed|specified|asked for) (?:the )?(?:form|shape|approach)|as instructed)\b/i],
|
|
124
|
+
['mechanical-test-update', /\b(mechanical(?:ly)? (?:test )?update|updated the (?:test )?(?:fixtures?|snapshots?)|snapshot refresh)\b/i],
|
|
125
|
+
['generated-refresh', /\b(regenerated?|generated-code refresh|re-?ran the generator|task (?:sync|generate-tools))\b/i],
|
|
126
|
+
['deterministic-config', /\b(one-line config|bumped the (?:version|constant)|flipped the (?:flag|setting))\b/i],
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
export interface Candidate {
|
|
130
|
+
id: string;
|
|
131
|
+
description: string;
|
|
132
|
+
/** Bracketed `[...]` content, or null. */
|
|
133
|
+
bracket: string | null;
|
|
134
|
+
/** The axis, when the bracket is an axis rather than a `forced:` constraint. */
|
|
135
|
+
axis: string | null;
|
|
136
|
+
/** The constraint, when the bracket declares a forced form. */
|
|
137
|
+
forced: string | null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface ParsedLine {
|
|
141
|
+
raw: string;
|
|
142
|
+
candidates: Candidate[];
|
|
143
|
+
chosen: string | null;
|
|
144
|
+
observation: string | null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export type FindingCode =
|
|
148
|
+
| 'missing-candidates'
|
|
149
|
+
| 'candidates-no-decision'
|
|
150
|
+
| 'missing-k0'
|
|
151
|
+
| 'k0-not-first'
|
|
152
|
+
| 'missing-axis'
|
|
153
|
+
| 'shared-axis'
|
|
154
|
+
| 'single-candidate-unforced'
|
|
155
|
+
| 'unknown-choice'
|
|
156
|
+
| 'missing-observation';
|
|
157
|
+
|
|
158
|
+
export interface Finding {
|
|
159
|
+
code: FindingCode;
|
|
160
|
+
message: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface Verdict {
|
|
164
|
+
findings: Finding[];
|
|
165
|
+
lines: ParsedLine[];
|
|
166
|
+
/** Which owed classes fired, for the report. */
|
|
167
|
+
owedClasses: string[];
|
|
168
|
+
/** Which exemptions fired. */
|
|
169
|
+
exemptClasses: string[];
|
|
170
|
+
owed: boolean;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** A candidate id: `K0`, or a single letter optionally followed by digits. */
|
|
174
|
+
const ID_RE = /^(K\d+|[A-Z]\d*)\b/;
|
|
175
|
+
|
|
176
|
+
/** Sentences that DENY rather than claim — mirrors the sibling linter. */
|
|
177
|
+
const DENIAL_RE = /\b(no|nothing|not|never|without)\b[^.]{0,40}$/i;
|
|
178
|
+
|
|
179
|
+
function fires(text: string, triggers: readonly (readonly [string, RegExp])[]): string[] {
|
|
180
|
+
const hit: string[] = [];
|
|
181
|
+
for (const sentence of text.split(/(?<=[.!?\n])/)) {
|
|
182
|
+
for (const [name, re] of triggers) {
|
|
183
|
+
if (!re.test(sentence)) continue;
|
|
184
|
+
const before = sentence.slice(0, sentence.search(re));
|
|
185
|
+
if (DENIAL_RE.test(before)) continue;
|
|
186
|
+
if (!hit.includes(name)) hit.push(name);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return hit;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function parseLine(rawBody: string): ParsedLine {
|
|
193
|
+
// A line the model wrapped in emphasis leaves its closing run at the end,
|
|
194
|
+
// where it would otherwise be read as part of the deciding observation.
|
|
195
|
+
const body = rawBody.replace(/(?:\*\*|__|\*|_)+[ \t]*$/, '').trim();
|
|
196
|
+
const arrowIdx = Math.max(body.indexOf('→'), body.indexOf('->'));
|
|
197
|
+
const listPart = arrowIdx === -1 ? body : body.slice(0, arrowIdx);
|
|
198
|
+
const decisionPart = arrowIdx === -1 ? '' : body.slice(arrowIdx).replace(/^(→|->)\s*/, '');
|
|
199
|
+
|
|
200
|
+
const candidates: Candidate[] = [];
|
|
201
|
+
for (const chunk of listPart.split(SLOT_SEPARATOR)) {
|
|
202
|
+
const t = chunk.trim();
|
|
203
|
+
if (t === '') continue;
|
|
204
|
+
const m = ID_RE.exec(t);
|
|
205
|
+
const id = m ? (m[1] as string) : '';
|
|
206
|
+
let rest = m ? t.slice((m[1] as string).length).trim() : t;
|
|
207
|
+
let bracket: string | null = null;
|
|
208
|
+
const b = /\[([^\]]*)\]\s*$/.exec(rest);
|
|
209
|
+
if (b) {
|
|
210
|
+
bracket = (b[1] ?? '').trim();
|
|
211
|
+
rest = rest.slice(0, b.index).trim();
|
|
212
|
+
}
|
|
213
|
+
const isForced = bracket !== null && /^forced\s*:/i.test(bracket);
|
|
214
|
+
candidates.push({
|
|
215
|
+
id,
|
|
216
|
+
description: rest,
|
|
217
|
+
bracket,
|
|
218
|
+
axis: bracket !== null && !isForced ? bracket : null,
|
|
219
|
+
forced: isForced ? (bracket as string).replace(/^forced\s*:\s*/i, '').trim() : null,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
let chosen: string | null = null;
|
|
224
|
+
let observation: string | null = null;
|
|
225
|
+
if (decisionPart !== '') {
|
|
226
|
+
const cm = ID_RE.exec(decisionPart.trim());
|
|
227
|
+
chosen = cm ? (cm[1] as string) : null;
|
|
228
|
+
const sep = decisionPart.search(/[;—]|\s-\s/);
|
|
229
|
+
observation = sep === -1 ? null : decisionPart.slice(sep + 1).replace(/^[—;\s-]+/, '').trim() || null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return { raw: body, candidates, chosen, observation };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The axis-distinctness threshold.
|
|
237
|
+
*
|
|
238
|
+
* A STATED DEFAULT, not a measured optimum. Two axes whose content words
|
|
239
|
+
* overlap at or above this read as one axis — step 2.5's "three candidates
|
|
240
|
+
* that differ only in where a helper lives are one candidate".
|
|
241
|
+
*/
|
|
242
|
+
export const AXIS_OVERLAP = 0.6;
|
|
243
|
+
|
|
244
|
+
export function checkReport(text: string): Verdict {
|
|
245
|
+
const findings: Finding[] = [];
|
|
246
|
+
const prepared = unwrapLines(stripFences(text));
|
|
247
|
+
|
|
248
|
+
const owedClasses = fires(prepared, OWED_CLASS_TRIGGERS);
|
|
249
|
+
const exemptClasses = fires(prepared, NOT_OWED_TRIGGERS);
|
|
250
|
+
// An exemption suppresses only when nothing owed fired. See NOT_OWED_TRIGGERS.
|
|
251
|
+
const owed = owedClasses.length > 0;
|
|
252
|
+
|
|
253
|
+
CANDIDATES_RE.lastIndex = 0;
|
|
254
|
+
const lines = [...prepared.matchAll(CANDIDATES_RE)].map((m) => parseLine(m[1] ?? ''));
|
|
255
|
+
|
|
256
|
+
if (owed && lines.length === 0) {
|
|
257
|
+
findings.push({
|
|
258
|
+
code: 'missing-candidates',
|
|
259
|
+
message:
|
|
260
|
+
`the report is in ${owedClasses.length} owed decision class(es) ` +
|
|
261
|
+
`(${owedClasses.join(', ')}) and carries no Candidates line. ` +
|
|
262
|
+
'K0 first, each non-K0 candidate with the axis it differs on, then the choice ' +
|
|
263
|
+
'and the observation that decided it.',
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for (const [i, l] of lines.entries()) {
|
|
268
|
+
const where = lines.length > 1 ? `line ${String(i + 1)} of ${String(lines.length)}: ` : '';
|
|
269
|
+
const ids = l.candidates.map((c) => c.id);
|
|
270
|
+
|
|
271
|
+
if (!ids.includes('K0')) {
|
|
272
|
+
findings.push({
|
|
273
|
+
code: 'missing-k0',
|
|
274
|
+
message:
|
|
275
|
+
`${where}no K0 candidate. K0 is "keep the current form / change nothing" and is ` +
|
|
276
|
+
'owed wherever the line is owed — without it the set assumes some edit is correct ' +
|
|
277
|
+
'before any reasoning starts.',
|
|
278
|
+
});
|
|
279
|
+
} else if (ids[0] !== 'K0') {
|
|
280
|
+
findings.push({ code: 'k0-not-first', message: `${where}K0 is present but not first (order: ${ids.join(', ')}).` });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const nonK0 = l.candidates.filter((c) => c.id !== 'K0');
|
|
284
|
+
const forcedDecl = l.candidates.find((c) => c.forced !== null);
|
|
285
|
+
|
|
286
|
+
if (nonK0.length === 0 && forcedDecl === undefined) {
|
|
287
|
+
findings.push({
|
|
288
|
+
code: 'single-candidate-unforced',
|
|
289
|
+
message:
|
|
290
|
+
`${where}only K0, and no forced-form constraint. Where exactly one form is ` +
|
|
291
|
+
'admissible, name the constraint that forecloses the alternatives — ' +
|
|
292
|
+
'`[forced: the base class fixes the extension point]`. A bare single candidate ' +
|
|
293
|
+
'is indistinguishable from not having looked.',
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
for (const c of nonK0) {
|
|
298
|
+
if (c.axis === null && c.forced === null) {
|
|
299
|
+
findings.push({
|
|
300
|
+
code: 'missing-axis',
|
|
301
|
+
message:
|
|
302
|
+
`${where}candidate ${c.id} carries no axis. Three candidates that differ only ` +
|
|
303
|
+
'in where a helper lives are one candidate.',
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const axes = nonK0.filter((c) => c.axis !== null).map((c) => ({ id: c.id, set: contentWords(c.axis as string) }));
|
|
309
|
+
for (let a = 0; a < axes.length; a++) {
|
|
310
|
+
for (let b = a + 1; b < axes.length; b++) {
|
|
311
|
+
const ov = overlap((axes[a] as { set: Set<string> }).set, (axes[b] as { set: Set<string> }).set);
|
|
312
|
+
if (ov >= AXIS_OVERLAP) {
|
|
313
|
+
findings.push({
|
|
314
|
+
code: 'shared-axis',
|
|
315
|
+
message:
|
|
316
|
+
`${where}candidates ${(axes[a] as { id: string }).id} and ${(axes[b] as { id: string }).id} ` +
|
|
317
|
+
`share an axis (overlap ${String(Math.round(ov * 1000) / 1000)}) — they count as ONE material candidate.`,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (l.chosen === null) {
|
|
324
|
+
findings.push({
|
|
325
|
+
code: 'candidates-no-decision',
|
|
326
|
+
message: `${where}no choice. The line ends with "→ <id>; <the observation that decided it>".`,
|
|
327
|
+
});
|
|
328
|
+
} else {
|
|
329
|
+
if (!ids.includes(l.chosen)) {
|
|
330
|
+
findings.push({
|
|
331
|
+
code: 'unknown-choice',
|
|
332
|
+
message: `${where}the choice ${l.chosen} is not one of the listed candidates (${ids.join(', ')}).`,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
if (l.observation === null || l.observation === '') {
|
|
336
|
+
findings.push({
|
|
337
|
+
code: 'missing-observation',
|
|
338
|
+
message:
|
|
339
|
+
`${where}the choice ${l.chosen} states no deciding observation. Without one the ` +
|
|
340
|
+
'choice reads as preference, which the rubric scores 2 rather than 3.',
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return { findings, lines, owedClasses, exemptClasses, owed };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** The three deterministic counts step 3.1 runs before any scoring. */
|
|
350
|
+
export interface CorpusCounts {
|
|
351
|
+
transcripts: number;
|
|
352
|
+
withLine: number;
|
|
353
|
+
withK0: number;
|
|
354
|
+
axesPairwiseDistinct: number;
|
|
355
|
+
k0Drawn: number;
|
|
356
|
+
findings: number;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Count a stored eval results file.
|
|
361
|
+
*
|
|
362
|
+
* Reads the same envelope `rdp_quality_eval --score-only` reads, so the
|
|
363
|
+
* treatment run needs no bespoke format. `k0Drawn` is the counter step 2.1
|
|
364
|
+
* actually wants: K0 present but never chosen across a whole corpus is K0 as
|
|
365
|
+
* an unchosen first row, which is the failure that step names.
|
|
366
|
+
*/
|
|
367
|
+
export function countCorpus(runPath: string): CorpusCounts | { error: string } {
|
|
368
|
+
let run: { results?: Array<{ variants?: Record<string, { text?: string }> }> };
|
|
369
|
+
try {
|
|
370
|
+
run = JSON.parse(fs.readFileSync(runPath, 'utf-8')) as typeof run;
|
|
371
|
+
} catch (e) {
|
|
372
|
+
return { error: `cannot read ${runPath}: ${String(e)}` };
|
|
373
|
+
}
|
|
374
|
+
const counts: CorpusCounts = {
|
|
375
|
+
transcripts: 0, withLine: 0, withK0: 0, axesPairwiseDistinct: 0, k0Drawn: 0, findings: 0,
|
|
376
|
+
};
|
|
377
|
+
for (const r of run.results ?? []) {
|
|
378
|
+
for (const v of Object.values(r.variants ?? {})) {
|
|
379
|
+
if (typeof v.text !== 'string' || v.text === '') continue;
|
|
380
|
+
counts.transcripts += 1;
|
|
381
|
+
const verdict = checkReport(v.text);
|
|
382
|
+
if (verdict.lines.length === 0) continue;
|
|
383
|
+
counts.withLine += 1;
|
|
384
|
+
counts.findings += verdict.findings.length;
|
|
385
|
+
const l = verdict.lines[0] as ParsedLine;
|
|
386
|
+
if (l.candidates.some((c) => c.id === 'K0')) counts.withK0 += 1;
|
|
387
|
+
if (!verdict.findings.some((f) => f.code === 'shared-axis')) counts.axesPairwiseDistinct += 1;
|
|
388
|
+
if (l.chosen === 'K0') counts.k0Drawn += 1;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return counts;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function readInput(argv: readonly string[]): { text: string } | { error: string } {
|
|
395
|
+
const fileIdx = argv.indexOf('--file');
|
|
396
|
+
if (fileIdx !== -1) {
|
|
397
|
+
const p = argv[fileIdx + 1];
|
|
398
|
+
if (p === undefined) return { error: '--file needs a path' };
|
|
399
|
+
try {
|
|
400
|
+
return { text: fs.readFileSync(path.resolve(p), 'utf-8') };
|
|
401
|
+
} catch (e) {
|
|
402
|
+
return { error: `cannot read ${p}: ${String(e)}` };
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (argv.includes('--stdin')) {
|
|
406
|
+
try {
|
|
407
|
+
return { text: fs.readFileSync(0, 'utf-8') };
|
|
408
|
+
} catch (e) {
|
|
409
|
+
return { error: `cannot read stdin: ${String(e)}` };
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return { error: 'usage: check_candidate_lines --stdin | --file <report.md> | --corpus <results.json>' };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
416
|
+
const corpusIdx = argv.indexOf('--corpus');
|
|
417
|
+
if (corpusIdx !== -1) {
|
|
418
|
+
const p = argv[corpusIdx + 1];
|
|
419
|
+
if (p === undefined) {
|
|
420
|
+
process.stderr.write('--corpus needs a results.json path\n');
|
|
421
|
+
return 1;
|
|
422
|
+
}
|
|
423
|
+
const c = countCorpus(path.resolve(p));
|
|
424
|
+
if ('error' in c) {
|
|
425
|
+
process.stderr.write(`${c.error}\n`);
|
|
426
|
+
return 1;
|
|
427
|
+
}
|
|
428
|
+
try {
|
|
429
|
+
assertScanned({
|
|
430
|
+
gate: 'check_candidate_lines',
|
|
431
|
+
scanned: c.transcripts,
|
|
432
|
+
units: 'transcript(s)',
|
|
433
|
+
roots: [p],
|
|
434
|
+
});
|
|
435
|
+
} catch (e) {
|
|
436
|
+
if (e instanceof DeadScopeError) {
|
|
437
|
+
process.stderr.write(`❌ ${e.message}\n`);
|
|
438
|
+
return 1;
|
|
439
|
+
}
|
|
440
|
+
throw e;
|
|
441
|
+
}
|
|
442
|
+
process.stdout.write(
|
|
443
|
+
`transcripts: ${String(c.transcripts)}\n` +
|
|
444
|
+
`Candidates line present: ${String(c.withLine)}\n` +
|
|
445
|
+
`K0 present: ${String(c.withK0)}\n` +
|
|
446
|
+
`axes pairwise distinct: ${String(c.axesPairwiseDistinct)}\n` +
|
|
447
|
+
`K0 actually drawn: ${String(c.k0Drawn)}\n` +
|
|
448
|
+
`shape findings: ${String(c.findings)}\n` +
|
|
449
|
+
`scanned: ${String(c.transcripts)}\n`,
|
|
450
|
+
);
|
|
451
|
+
return 0;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const quiet = argv.includes('--quiet');
|
|
455
|
+
const input = readInput(argv);
|
|
456
|
+
if ('error' in input) {
|
|
457
|
+
process.stderr.write(`${input.error}\n`);
|
|
458
|
+
return 1;
|
|
459
|
+
}
|
|
460
|
+
const contentLines = input.text.split('\n').filter((l) => l.trim() !== '').length;
|
|
461
|
+
try {
|
|
462
|
+
assertScanned({ gate: 'check_candidate_lines', scanned: contentLines, units: 'report line(s)', roots: ['<stdin>'] });
|
|
463
|
+
} catch (e) {
|
|
464
|
+
if (e instanceof DeadScopeError) {
|
|
465
|
+
process.stderr.write(`❌ ${e.message}\n`);
|
|
466
|
+
return 1;
|
|
467
|
+
}
|
|
468
|
+
throw e;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const verdict = checkReport(input.text);
|
|
472
|
+
for (const f of verdict.findings) process.stderr.write(`❌ ${f.code} ${f.message}\n`);
|
|
473
|
+
if (verdict.findings.length > 0) {
|
|
474
|
+
process.stderr.write(`\n❌ check_candidate_lines: ${String(verdict.findings.length)} finding(s)\n`);
|
|
475
|
+
return 2;
|
|
476
|
+
}
|
|
477
|
+
if (!quiet) {
|
|
478
|
+
if (!verdict.owed && verdict.lines.length === 0) {
|
|
479
|
+
process.stdout.write(
|
|
480
|
+
'✅ check_candidate_lines: no owed decision class detected in this report' +
|
|
481
|
+
(verdict.exemptClasses.length > 0 ? ` (exempt: ${verdict.exemptClasses.join(', ')})` : '') +
|
|
482
|
+
'\n',
|
|
483
|
+
);
|
|
484
|
+
} else {
|
|
485
|
+
process.stdout.write(
|
|
486
|
+
`✅ check_candidate_lines: ${String(verdict.lines.length)} well-formed line(s)` +
|
|
487
|
+
(verdict.owedClasses.length > 0 ? ` for owed class(es) ${verdict.owedClasses.join(', ')}` : '') +
|
|
488
|
+
'\n',
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return 0;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
496
|
+
process.exit(main());
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export { REPO_ROOT };
|