@event4u/agent-config 9.28.0 → 9.30.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/.claude-plugin/marketplace.json +1 -1
- package/AGENTS.md +6 -6
- package/CHANGELOG.md +172 -5
- package/README.md +2 -1
- package/agents/templates/.ai-council.yml.example +55 -24
- package/dist/agent-src/commands/analyze/inbox.md +22 -1
- package/dist/agent-src/commands/cost/report.md +2 -2
- package/dist/agent-src/commands/explain-run.md +31 -1
- package/dist/agent-src/commands/team/adversarial.md +8 -7
- package/dist/agent-src/commands/team/delegate.md +14 -12
- package/dist/agent-src/commands/team/review.md +9 -8
- package/dist/agent-src/commands/team/status.md +9 -8
- package/dist/agent-src/commands/team.md +29 -19
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +88 -2
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +29 -21
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +26 -15
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +22 -1
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +17 -0
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +65 -1
- package/dist/agent-src/contexts/execution/subagent-routing.md +13 -11
- package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +25 -0
- package/dist/agent-src/contexts/execution/subagent-steering.md +6 -3
- package/dist/agent-src/rules/delegation-policy.md +11 -7
- package/dist/agent-src/rules/preservation-guard.md +9 -0
- package/dist/agent-src/rules/settings-ask-protocol.md +8 -7
- package/dist/agent-src/rules/token-budget-discipline.md +1 -1
- package/dist/agent-src/scripts/roadmap_gates.ts +312 -0
- package/dist/agent-src/scripts/update_roadmap_progress.ts +72 -4
- package/dist/agent-src/skills/forensics-report/SKILL.md +114 -0
- package/dist/agent-src/skills/forensics-report/evals/triggers.json +17 -0
- package/dist/agent-src/skills/premortem/SKILL.md +30 -1
- package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -1
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +17 -5
- package/dist/agent-src/skills/subagent-orchestration/schemas/subagent-status.json +61 -3
- package/dist/agent-src/skills/test-case-discovery/SKILL.md +1 -1
- package/dist/agent-src/templates/AGENTS.md +4 -4
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +23 -0
- package/dist/cli/registry.js +2 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-22GCIJ2O.js → chunk-66OVWGB5.js} +1 -1
- package/dist/cli-delegate/{chunk-7OMQCH4V.js → chunk-BTJNRUX3.js} +22 -35
- package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-DDUNNFYX.js} +24 -17
- package/dist/cli-delegate/{chunk-PQSAQKHW.js → chunk-EQY4FLJB.js} +3 -3
- package/dist/cli-delegate/{chunk-2BE5I2MN.js → chunk-IYTGIG2Y.js} +155 -91
- package/dist/cli-delegate/{chunk-GB2FSUEE.js → chunk-LCFJIQ4J.js} +32 -0
- package/dist/cli-delegate/{chunk-VSYT7OHO.js → chunk-LVRYBKTI.js} +31 -2
- package/dist/cli-delegate/chunk-MMJL4TIX.js +18 -0
- package/dist/cli-delegate/{chunk-JXARCGVN.js → chunk-MP3JT3IU.js} +1 -1
- package/dist/cli-delegate/chunk-O3CO5BFB.js +161 -0
- package/dist/cli-delegate/{chunk-REKCMYOU.js → chunk-PFBV6XRT.js} +36 -20
- package/dist/cli-delegate/chunk-RVGBH4XK.js +142 -0
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +2803 -137
- package/dist/cli-delegate/cmd_doctor.js +10 -5
- package/dist/cli-delegate/cmd_explain.js +2 -2
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +88 -8
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_route_audit.js +3 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +177 -0
- package/dist/cli-delegate/cmd_settings_get.js +2 -8
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_settings_set.js +2 -2
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +72 -23
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -2
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +39 -2
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +12 -4
- package/dist/hooks/dispatch.js +2278 -611
- package/dist/install/install.mjs +56 -17
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +4122 -699
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +54 -0
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/scripts/condense.js +147 -2
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +12 -7
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/shared/settingsCarveOut.js +0 -9
- package/dist/shared/settingsCarveOut.js.map +1 -1
- package/dist/shared/settingsClasses.js +11 -9
- package/dist/shared/settingsClasses.js.map +1 -1
- package/dist/ui/assets/index-B5qGYOJo.js +26 -0
- package/dist/ui/assets/index-B5qGYOJo.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/contracts/settings-classes.md +58 -12
- package/package.json +1 -1
- package/src/agent-src/scripts/roadmap_gates.ts +312 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +72 -4
- package/src/agent-src/templates/AGENTS.md +4 -4
- package/src/config/agent-settings.template.yml +88 -66
- package/src/config/discovery/packs.yml +11 -0
- package/src/config/discovery/workspaces.yml +1 -1
- package/src/config/dispatch-economy-metrics.json +61 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/hook-token-budget.json +91 -0
- package/src/config/recycle-threshold-budget.json +12 -0
- package/src/config/rule-enforcement-baseline.json +86 -0
- package/src/scripts/_cli/cmd_conformance.ts +31 -44
- package/src/scripts/_cli/cmd_doctor.ts +65 -48
- package/src/scripts/_cli/cmd_session_recycle.ts +226 -0
- package/src/scripts/_dispatch.bash +23 -0
- package/src/scripts/_lib/agent_settings.ts +58 -0
- package/src/scripts/_lib/auto_dispatch.corpus.test.ts +1 -1
- package/src/scripts/_lib/auto_dispatch.ts +23 -18
- package/src/scripts/_lib/capsule_trigger.ts +124 -0
- package/src/scripts/_lib/host_capability.ts +63 -6
- package/src/scripts/_lib/judgment_ladder.ts +634 -0
- package/src/scripts/_lib/orchestration_gate.ts +8 -0
- package/src/scripts/_lib/orchestration_record.ts +78 -10
- package/src/scripts/_lib/orchestration_savings.ts +1 -1
- package/src/scripts/_lib/recycle_envelope_paths.ts +35 -0
- package/src/scripts/_lib/self_repair.ts +354 -13
- package/src/scripts/_lib/self_repair_store.ts +28 -0
- package/src/scripts/_lib/session_eol.ts +230 -0
- package/src/scripts/_lib/session_role.ts +42 -0
- package/src/scripts/_lib/subagent_capsule.ts +341 -0
- package/src/scripts/_lib/subagent_response.ts +94 -0
- package/src/scripts/_lib/subagent_spawn.ts +13 -0
- package/src/scripts/_lib/subagent_steering.ts +9 -4
- package/src/scripts/_lib/tier_budget_routing.ts +6 -1
- package/src/scripts/_lib/worker_budget.ts +37 -2
- package/src/scripts/ai_council/clients.ts +126 -55
- package/src/scripts/ai_council/config.ts +94 -6
- package/src/scripts/ai_council/handoff.ts +113 -0
- package/src/scripts/ai_council/modes.ts +19 -4
- package/src/scripts/ai_council/orchestrator.ts +105 -0
- package/src/scripts/ai_council/quorum.ts +84 -0
- package/src/scripts/ai_council/session.ts +44 -0
- package/src/scripts/ai_council/transport_resolver.ts +127 -1
- package/src/scripts/ai_team/availability.ts +110 -0
- package/src/scripts/ai_team/config.ts +34 -14
- package/src/scripts/ai_team/team_dispatch.ts +71 -29
- package/src/scripts/ask_transport.ts +221 -0
- package/src/scripts/bench_critic_protocol.ts +396 -0
- package/src/scripts/bench_hook_injection.ts +234 -0
- package/src/scripts/check_adr_frontmatter.ts +84 -40
- package/src/scripts/check_branch_freshness.ts +1 -0
- package/src/scripts/check_corpus_staleness.ts +18 -4
- package/src/scripts/check_detector_corpus.ts +219 -0
- package/src/scripts/check_gitignore_freshness.ts +42 -8
- package/src/scripts/check_hook_bundle_freshness.ts +24 -1
- package/src/scripts/check_kernel_prefix_stability.ts +1 -0
- package/src/scripts/check_standing_rule_delivery.ts +73 -32
- package/src/scripts/check_static_layer_stability.ts +154 -0
- package/src/scripts/check_suppression_hygiene.ts +48 -0
- package/src/scripts/condense.ts +162 -2
- package/src/scripts/council_cli.ts +401 -25
- package/src/scripts/dispatch_economy_report.ts +448 -0
- package/src/scripts/dispatch_r2_reviewer.ts +15 -2
- package/src/scripts/explain_run.ts +206 -4
- package/src/scripts/forensics_report.ts +333 -0
- package/src/scripts/generate_settings_reference.ts +1 -0
- package/src/scripts/handoff_context_hook.ts +112 -4
- package/src/scripts/hook_manifest.yaml +117 -10
- package/src/scripts/hooks/concern_registry.ts +8 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +106 -35
- package/src/scripts/hooks/dispatch_hook.ts +39 -2
- package/src/scripts/hooks/edit_shape_hook.ts +303 -0
- package/src/scripts/hooks/orchestration_record_hook.ts +19 -1
- package/src/scripts/hooks/reread_guard_hook.ts +314 -0
- package/src/scripts/hooks/rtk_wrap_hook.ts +133 -14
- package/src/scripts/hooks/session_eol_hook.ts +268 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +507 -0
- package/src/scripts/install-hooks.sh +1 -1
- package/src/scripts/language_mirror_hook.ts +89 -0
- package/src/scripts/lint_decision_impact.ts +14 -1
- package/src/scripts/lint_discovery_vocabulary.ts +2 -0
- package/src/scripts/lint_hook_manifest.ts +82 -0
- package/src/scripts/lint_never_silent.ts +276 -0
- package/src/scripts/lint_no_activation_gates.ts +271 -0
- package/src/scripts/lint_readme_serial_comma.ts +1 -0
- package/src/scripts/lint_rule_enforcement_declaration.ts +347 -0
- package/src/scripts/measure_turn_end_gate.ts +194 -0
- package/src/scripts/orchestration_record.ts +12 -1
- package/src/scripts/rebuild_hook_bundle.ts +178 -0
- package/src/scripts/release.ts +74 -2
- package/src/scripts/report_conformance_funnel.ts +284 -0
- package/src/scripts/report_imperative_density.ts +1 -0
- package/src/scripts/routing_doctor.ts +118 -36
- package/src/scripts/score_skill_selection.ts +19 -5
- package/src/scripts/self_repair_cli.ts +337 -59
- package/src/scripts/session_eol_report.ts +261 -0
- package/src/scripts/team_events_spike.ts +271 -0
- package/src/scripts/team_review_gate_hook.ts +31 -9
- package/src/server/schemas/settings.ts +24 -17
- package/src/shared/settingsCarveOut.ts +0 -10
- package/src/shared/settingsClasses.ts +11 -9
- package/src/templates/compact-instructions.md +40 -0
- package/dist/ui/assets/index-DmR0mbk4.js +0 -26
- package/dist/ui/assets/index-DmR0mbk4.js.map +0 -1
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `enforced_by`-declaration ratchet — a NEW rule states its enforcement, or
|
|
4
|
+
* states the gap; only the legacy set may stay silent.
|
|
5
|
+
*
|
|
6
|
+
* The enforcement-coverage number moved backwards across 9.23→9.29 (13.5% →
|
|
7
|
+
* 12.9%) for exactly one reason: rules were added faster than backstops, and
|
|
8
|
+
* nothing required a new rule to even *declare* where it stands. Coverage
|
|
9
|
+
* itself cannot be gated (whether a backstop is buildable is a per-rule
|
|
10
|
+
* judgment), but the *declaration* can: `enforced_by:` accepts an honest
|
|
11
|
+
* `none`, so requiring the key costs a new rule's author one line of truth,
|
|
12
|
+
* never a mechanism.
|
|
13
|
+
*
|
|
14
|
+
* Ratchet shape (the established baseline-the-legacy pattern):
|
|
15
|
+
* - The 84 rules that predate this gate are recorded in
|
|
16
|
+
* `src/config/rule-enforcement-baseline.json`. They stay legal while
|
|
17
|
+
* undeclared; the list may only shrink.
|
|
18
|
+
* - A rule NOT in the baseline must carry `enforced_by:` — a validator/
|
|
19
|
+
* hook/test/observer reference, or the explicit `none`.
|
|
20
|
+
* - A rule declaring `none` must also state the gap in its body (the
|
|
21
|
+
* `enforced_by: none` honesty convention the existing none-rules use) —
|
|
22
|
+
* a frontmatter-only `none` nobody reads is a silent gap again.
|
|
23
|
+
* - A baseline entry whose rule gained a declaration (or was deleted) must
|
|
24
|
+
* leave the baseline in the same change — a stale entry is a freed slot
|
|
25
|
+
* a later silent rule could hide in.
|
|
26
|
+
*
|
|
27
|
+
* Deliberately NOT checked here: whether a declared validator resolves, is
|
|
28
|
+
* wired, or can fail a build — that is `check_enforcement_coverage`'s job
|
|
29
|
+
* (resolution), and `check_backstop_debt`'s (red backstops). This gate owns
|
|
30
|
+
* the declaration boundary only, on `src/rules/` only — gate scripts and
|
|
31
|
+
* their `// ledger-exempt:` markers are a different surface
|
|
32
|
+
* (`check_gate_completeness`) and are not read here.
|
|
33
|
+
*
|
|
34
|
+
* **Where the baseline lives, and why it ships.** `src/config/` is a directory
|
|
35
|
+
* entry in `package.json`'s `files[]`, so `rule-enforcement-baseline.json` is in
|
|
36
|
+
* the published tarball alongside its nine siblings — `gate-violation-baselines`,
|
|
37
|
+
* the four `*-budget` files, and the rest. All ten are repo-internal gate state:
|
|
38
|
+
* this gate is their consumer, it ships too (via `src/scripts/`), and in a
|
|
39
|
+
* consumer checkout it finds no `src/rules/` and does nothing. That is inert, not
|
|
40
|
+
* orphaned, and it is the pre-existing shape rather than something this file
|
|
41
|
+
* introduced — an automated review flagged it as "ships into dist with no
|
|
42
|
+
* consumer", which is doubly wrong: the file is not in `dist/` at all, and the
|
|
43
|
+
* consumer is this script. Whether ten repo-internal baselines belong in the
|
|
44
|
+
* tarball is a real packaging question; it is one decision about ten files, not a
|
|
45
|
+
* carve-out for the tenth.
|
|
46
|
+
*
|
|
47
|
+
* CLI contract: exit 0 = clean, 1 = a finding (or dead scan root), 2 = usage.
|
|
48
|
+
* `--quiet` mutes the clean-path line; `--root <dir>` re-roots for fixtures;
|
|
49
|
+
* `--write-baseline` records the current undeclared set (bootstrap/re-anchor —
|
|
50
|
+
* a deliberate maintainer act, never CI).
|
|
51
|
+
*/
|
|
52
|
+
import * as fs from 'node:fs';
|
|
53
|
+
import * as os from 'node:os';
|
|
54
|
+
import * as path from 'node:path';
|
|
55
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
56
|
+
|
|
57
|
+
import { read_frontmatter } from './check_enforcement_coverage.js';
|
|
58
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
59
|
+
import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
|
|
60
|
+
import { is_kernel_rule } from './_lib/kernel_rules.js';
|
|
61
|
+
import { DeadScopeError, reportScanned } from './_lib/scan_scope.js';
|
|
62
|
+
|
|
63
|
+
const GATE = 'lint_rule_enforcement_declaration';
|
|
64
|
+
|
|
65
|
+
const QUIET = process.argv.slice(2).includes('--quiet');
|
|
66
|
+
|
|
67
|
+
const _HERE = path.resolve(fileURLToPath(import.meta.url));
|
|
68
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
69
|
+
|
|
70
|
+
const RULES_RELATIVE = path.join('src', 'rules');
|
|
71
|
+
const BASELINE_RELATIVE = path.join('src', 'config', 'rule-enforcement-baseline.json');
|
|
72
|
+
|
|
73
|
+
function _rootOverride(argv: readonly string[]): string | null {
|
|
74
|
+
const i = argv.indexOf('--root');
|
|
75
|
+
const value = i === -1 ? undefined : argv[i + 1];
|
|
76
|
+
return value === undefined ? null : path.resolve(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** enforced_by as declared, normalised to a string array; null = absent. */
|
|
80
|
+
function _declaration(fm: Record<string, string | string[]>): string[] | null {
|
|
81
|
+
const raw = fm['enforced_by'];
|
|
82
|
+
if (raw === undefined) return null;
|
|
83
|
+
const list = Array.isArray(raw) ? raw : [raw];
|
|
84
|
+
return list.length === 0 ? null : list;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The body must state a `none` gap in prose — the honesty convention. */
|
|
88
|
+
function _bodyStatesNone(text: string): boolean {
|
|
89
|
+
const end = text.indexOf('\n---\n', 4);
|
|
90
|
+
const body = end === -1 ? text : text.slice(end + 5);
|
|
91
|
+
return body.includes('enforced_by: none') || body.includes('`enforced_by: none`');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function _readBaseline(root: string): string[] | null {
|
|
95
|
+
try {
|
|
96
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(path.join(root, BASELINE_RELATIVE), 'utf-8'));
|
|
97
|
+
if (!Array.isArray(parsed) || !parsed.every((e) => typeof e === 'string')) return null;
|
|
98
|
+
return parsed as string[];
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface Findings {
|
|
105
|
+
findings: string[];
|
|
106
|
+
undeclared: string[];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function _scan(root: string, baseline: ReadonlySet<string>, ledger: GateLedger | null): Findings {
|
|
110
|
+
const rulesDir = path.join(root, RULES_RELATIVE);
|
|
111
|
+
const files = fs
|
|
112
|
+
.readdirSync(rulesDir)
|
|
113
|
+
.filter((f) => f.endsWith('.md'))
|
|
114
|
+
.sort();
|
|
115
|
+
ledger?.plan(files);
|
|
116
|
+
|
|
117
|
+
const findings: string[] = [];
|
|
118
|
+
const undeclared: string[] = [];
|
|
119
|
+
const seen = new Set<string>();
|
|
120
|
+
|
|
121
|
+
for (const file of files) {
|
|
122
|
+
seen.add(file);
|
|
123
|
+
const text = fs.readFileSync(path.join(rulesDir, file), 'utf-8');
|
|
124
|
+
const decl = _declaration(read_frontmatter(text));
|
|
125
|
+
|
|
126
|
+
if (decl === null) {
|
|
127
|
+
undeclared.push(file);
|
|
128
|
+
if (baseline.has(file)) {
|
|
129
|
+
ledger?.outOfScope(file, 'declared_exemption');
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const f =
|
|
133
|
+
`src/rules/${file} declares no \`enforced_by:\`. A new rule states its enforcement ` +
|
|
134
|
+
`(validator:/hook:/test:/observer:) or states the gap (\`none\` + a body line saying so). ` +
|
|
135
|
+
`The baseline covers only rules that predate this gate.`;
|
|
136
|
+
findings.push(f);
|
|
137
|
+
ledger?.fail(file, f);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Kernel rules are exempt from the body-prose convention, derived from
|
|
142
|
+
// _lib/kernel_rules.ts (never a hardcoded list) so the exemption closes
|
|
143
|
+
// itself the moment a rule leaves the kernel — the same self-closing
|
|
144
|
+
// shape validate_frontmatter uses for obligation_frequency. Their
|
|
145
|
+
// projected bytes are stability-gated, so requiring a prose line here
|
|
146
|
+
// would force an un-editable edit.
|
|
147
|
+
if (decl.includes('none') && !_bodyStatesNone(text) && !is_kernel_rule(file)) {
|
|
148
|
+
const f =
|
|
149
|
+
`src/rules/${file} declares \`enforced_by: none\` in frontmatter but never states the gap ` +
|
|
150
|
+
`in its body — the honesty convention is one prose line naming \`enforced_by: none\`, so a ` +
|
|
151
|
+
`reader of the rule sees the gap, not only a machine.`;
|
|
152
|
+
findings.push(f);
|
|
153
|
+
ledger?.fail(file, f);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
ledger?.complete(file);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const entry of [...baseline].sort()) {
|
|
161
|
+
if (!seen.has(entry)) {
|
|
162
|
+
findings.push(
|
|
163
|
+
`${BASELINE_RELATIVE} baseline entry \`${entry}\` names no existing rule — remove it in the ` +
|
|
164
|
+
`same change that renamed/deleted the rule. A stale entry is a freed slot a later ` +
|
|
165
|
+
`undeclared rule could silently occupy.`,
|
|
166
|
+
);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (!undeclared.includes(entry)) {
|
|
170
|
+
findings.push(
|
|
171
|
+
`${BASELINE_RELATIVE} baseline entry \`${entry}\` now declares \`enforced_by:\` — remove it ` +
|
|
172
|
+
`from the baseline in the same change. The list may only shrink.`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { findings, undeclared };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Prove against the real CLI that the ratchet rejects what it must. */
|
|
181
|
+
export function selfTest(): number {
|
|
182
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'lred-selftest-'));
|
|
183
|
+
const declared = '---\ntype: "auto"\nenforced_by:\n - "validator:src/scripts/example.ts"\n---\n\n# R\n\nBody.\n';
|
|
184
|
+
const noneHonest =
|
|
185
|
+
'---\ntype: "auto"\nenforced_by:\n - "none"\n---\n\n# R\n\nThis rule ships `enforced_by: none` — the obligation is model-carried.\n';
|
|
186
|
+
const noneSilent = '---\ntype: "auto"\nenforced_by:\n - "none"\n---\n\n# R\n\nBody with no gap statement.\n';
|
|
187
|
+
const silent = '---\ntype: "auto"\n---\n\n# R\n\nBody.\n';
|
|
188
|
+
|
|
189
|
+
const seed = (name: string, rules: Record<string, string>, baseline: string[]): string => {
|
|
190
|
+
const root = path.join(tmp, name);
|
|
191
|
+
fs.mkdirSync(path.join(root, RULES_RELATIVE), { recursive: true });
|
|
192
|
+
fs.mkdirSync(path.join(root, 'src', 'config'), { recursive: true });
|
|
193
|
+
for (const [file, text] of Object.entries(rules)) {
|
|
194
|
+
fs.writeFileSync(path.join(root, RULES_RELATIVE, file), text, 'utf-8');
|
|
195
|
+
}
|
|
196
|
+
fs.writeFileSync(path.join(root, BASELINE_RELATIVE), `${JSON.stringify(baseline, null, 2)}\n`, 'utf-8');
|
|
197
|
+
return root;
|
|
198
|
+
};
|
|
199
|
+
const run = (cwd: string): number =>
|
|
200
|
+
runGateCli(REPO_ROOT, `src/scripts/${GATE}.ts`, ['--quiet', '--root', cwd], REPO_ROOT);
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
return runSelfTest({
|
|
204
|
+
gate: GATE,
|
|
205
|
+
minCases: 6,
|
|
206
|
+
minRejectCases: 4,
|
|
207
|
+
cases: [
|
|
208
|
+
{
|
|
209
|
+
name: 'a declared new rule and a baselined legacy rule pass',
|
|
210
|
+
expect: 'accept',
|
|
211
|
+
run: () => run(seed('clean', { 'a.md': declared, 'legacy.md': silent }, ['legacy.md'])),
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: 'an honest none (frontmatter + body gap line) passes',
|
|
215
|
+
expect: 'accept',
|
|
216
|
+
run: () => run(seed('none-honest', { 'a.md': noneHonest }, [])),
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: 'a NEW rule without enforced_by is rejected',
|
|
220
|
+
expect: 'reject',
|
|
221
|
+
run: () => run(seed('new-silent', { 'a.md': silent }, [])),
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: 'a none declaration without the body gap line is rejected',
|
|
225
|
+
expect: 'reject',
|
|
226
|
+
run: () => run(seed('none-silent', { 'a.md': noneSilent }, [])),
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: 'a stale baseline entry (rule deleted/renamed) is rejected',
|
|
230
|
+
expect: 'reject',
|
|
231
|
+
run: () => run(seed('stale', { 'a.md': declared }, ['gone.md'])),
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: 'a baseline entry whose rule gained a declaration is rejected until removed',
|
|
235
|
+
expect: 'reject',
|
|
236
|
+
run: () => run(seed('shrink', { 'a.md': declared }, ['a.md'])),
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: 'a dead scan root is rejected, never passed as nothing-to-check',
|
|
240
|
+
expect: 'reject',
|
|
241
|
+
run: () => run(path.join(tmp, 'dead-root-does-not-exist')),
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
});
|
|
245
|
+
} finally {
|
|
246
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function main(): number {
|
|
251
|
+
const argv = process.argv.slice(2);
|
|
252
|
+
if (argv.includes('--self-test')) {
|
|
253
|
+
return selfTest();
|
|
254
|
+
}
|
|
255
|
+
const root = _rootOverride(argv) ?? REPO_ROOT;
|
|
256
|
+
|
|
257
|
+
const rulesDir = path.join(root, RULES_RELATIVE);
|
|
258
|
+
let isDir = false;
|
|
259
|
+
try {
|
|
260
|
+
isDir = fs.statSync(rulesDir).isDirectory();
|
|
261
|
+
} catch {
|
|
262
|
+
isDir = false;
|
|
263
|
+
}
|
|
264
|
+
if (!isDir) {
|
|
265
|
+
process.stderr.write(`❌ ${GATE}: rules dir not found at ${RULES_RELATIVE} — the scan root is dead.\n`);
|
|
266
|
+
return 1;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (argv.includes('--write-baseline')) {
|
|
270
|
+
const { undeclared } = _scan(root, new Set(), null);
|
|
271
|
+
fs.writeFileSync(
|
|
272
|
+
path.join(root, BASELINE_RELATIVE),
|
|
273
|
+
`${JSON.stringify(undeclared, null, 2)}\n`,
|
|
274
|
+
'utf-8',
|
|
275
|
+
);
|
|
276
|
+
process.stdout.write(`${GATE}: baseline written — ${undeclared.length} legacy undeclared rule(s).\n`);
|
|
277
|
+
return 0;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const baseline = _readBaseline(root);
|
|
281
|
+
if (baseline === null) {
|
|
282
|
+
process.stderr.write(
|
|
283
|
+
`❌ ${GATE}: ${BASELINE_RELATIVE} missing or not a string array — run --write-baseline once ` +
|
|
284
|
+
`(a deliberate maintainer act) or restore the committed file.\n`,
|
|
285
|
+
);
|
|
286
|
+
return 1;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const ledger = new GateLedger(GATE);
|
|
290
|
+
const { findings } = _scan(root, new Set(baseline), ledger);
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
reportScanned({
|
|
294
|
+
gate: GATE,
|
|
295
|
+
scanned: fs.readdirSync(rulesDir).filter((f) => f.endsWith('.md')).length,
|
|
296
|
+
units: 'rule file(s)',
|
|
297
|
+
roots: [RULES_RELATIVE],
|
|
298
|
+
});
|
|
299
|
+
} catch (e) {
|
|
300
|
+
if (e instanceof DeadScopeError) {
|
|
301
|
+
process.stderr.write(`❌ ${e.message}\n`);
|
|
302
|
+
return 1;
|
|
303
|
+
}
|
|
304
|
+
throw e;
|
|
305
|
+
}
|
|
306
|
+
ledger.report();
|
|
307
|
+
|
|
308
|
+
if (findings.length > 0) {
|
|
309
|
+
for (const f of findings) {
|
|
310
|
+
process.stderr.write(`❌ ${f}\n`);
|
|
311
|
+
}
|
|
312
|
+
return 1;
|
|
313
|
+
}
|
|
314
|
+
if (!QUIET) {
|
|
315
|
+
process.stdout.write(
|
|
316
|
+
`✅ ${GATE}: every non-baselined rule declares its enforcement (baseline: ${baseline.length}, shrink-only).\n`,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
return 0;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
323
|
+
function _isCliEntry(): boolean {
|
|
324
|
+
if (typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
if (process.argv[1] === undefined) {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
331
|
+
if (import.meta.url === argvUrl) {
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
336
|
+
const argv1 = fs.realpathSync(path.resolve(process.argv[1]));
|
|
337
|
+
return here === argv1;
|
|
338
|
+
} catch {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (_isCliEntry()) {
|
|
344
|
+
process.exit(main());
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export { GATE, REPO_ROOT, BASELINE_RELATIVE, main };
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env -S npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* `measure-turn-end-gate` — the false-positive pass roadmap
|
|
4
|
+
* `road-to-conformance-round5.md` § 3.4 asks for, run against a REAL
|
|
5
|
+
* transcript corpus rather than a hand-written one.
|
|
6
|
+
*
|
|
7
|
+
* Why this exists as a script and not as a number in a commit message: the
|
|
8
|
+
* roadmap asks to "record measured precision per detector", and a recorded
|
|
9
|
+
* figure nobody can re-derive is the same class of claim this whole round
|
|
10
|
+
* exists to remove. One command, same store, same numbers.
|
|
11
|
+
*
|
|
12
|
+
* What it measures, and what it deliberately does NOT:
|
|
13
|
+
*
|
|
14
|
+
* · FIRE RATE of each detector over every assistant turn in the window.
|
|
15
|
+
* · For detector B, the CROSS-TAB against `conformance_scan`'s own
|
|
16
|
+
* independent language check. That scanner reads only the first prose
|
|
17
|
+
* line and this detector reads the whole stripped reply, so the two are
|
|
18
|
+
* related instruments, not the same one — which is exactly what makes
|
|
19
|
+
* the disagreement cells informative.
|
|
20
|
+
* · It does NOT compute recall against the 20 promissory occurrences the
|
|
21
|
+
* round-5 audit reported. Those came from subagent transcript READING;
|
|
22
|
+
* `conformance_scan.ts` states in its own header that promissory
|
|
23
|
+
* closings are "deliberately NOT scanned", so there is no machine-
|
|
24
|
+
* readable ground truth to score against. Claiming a recall figure here
|
|
25
|
+
* would invent one.
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
* ./scripts-run src/scripts/measure_turn_end_gate --store <dir> [--limit N]
|
|
29
|
+
*/
|
|
30
|
+
import * as fs from 'node:fs';
|
|
31
|
+
import * as path from 'node:path';
|
|
32
|
+
|
|
33
|
+
import { classify } from './language_mirror_hook.js';
|
|
34
|
+
import { assistantText, isInjectedBody, scanSession, userText } from './conformance_scan.js';
|
|
35
|
+
import { detectLanguage, detectPromissory } from './hooks/turn_end_gate_hook.js';
|
|
36
|
+
|
|
37
|
+
interface Counts {
|
|
38
|
+
assistant_turns: number;
|
|
39
|
+
promissory_fires: number;
|
|
40
|
+
language_fires: number;
|
|
41
|
+
/** Detector B fired AND the scanner also flagged that session-turn. */
|
|
42
|
+
language_agree: number;
|
|
43
|
+
/** Detector B fired where the scanner saw nothing — candidate false positives. */
|
|
44
|
+
language_only: number;
|
|
45
|
+
sessions: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sessionFiles(store: string, limit: number): string[] {
|
|
49
|
+
let entries: string[];
|
|
50
|
+
try {
|
|
51
|
+
entries = fs.readdirSync(store).filter((f) => f.endsWith('.jsonl'));
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
const withTime = entries.map((f) => {
|
|
56
|
+
const p = path.join(store, f);
|
|
57
|
+
let mtime = 0;
|
|
58
|
+
try {
|
|
59
|
+
mtime = fs.statSync(p).mtimeMs;
|
|
60
|
+
} catch {
|
|
61
|
+
/* unreadable — sorts last */
|
|
62
|
+
}
|
|
63
|
+
return { p, mtime };
|
|
64
|
+
});
|
|
65
|
+
withTime.sort((a, b) => b.mtime - a.mtime);
|
|
66
|
+
return withTime.slice(0, limit).map((e) => e.p);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function measure(store: string, limit: number): Counts {
|
|
70
|
+
const c: Counts = {
|
|
71
|
+
assistant_turns: 0,
|
|
72
|
+
promissory_fires: 0,
|
|
73
|
+
language_fires: 0,
|
|
74
|
+
language_agree: 0,
|
|
75
|
+
language_only: 0,
|
|
76
|
+
sessions: 0,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
for (const file of sessionFiles(store, limit)) {
|
|
80
|
+
let lines: string[];
|
|
81
|
+
try {
|
|
82
|
+
lines = fs.readFileSync(file, 'utf-8').split('\n').filter((l) => l.trim() !== '');
|
|
83
|
+
} catch {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
c.sessions += 1;
|
|
87
|
+
const sessionId = path.basename(file).slice(0, 8);
|
|
88
|
+
|
|
89
|
+
// The scanner's independent verdict for this session, keyed by the
|
|
90
|
+
// timestamp-free count of language violations. Used only as a
|
|
91
|
+
// cross-tab total, never as ground truth for this detector.
|
|
92
|
+
let scannerLanguage = 0;
|
|
93
|
+
try {
|
|
94
|
+
scannerLanguage = scanSession(sessionId, lines).violations.filter(
|
|
95
|
+
(v) => v.check === 'language-pin',
|
|
96
|
+
).length;
|
|
97
|
+
} catch {
|
|
98
|
+
scannerLanguage = 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let pinned: 'de' | 'en' | 'und' = 'und';
|
|
102
|
+
let myLanguage = 0;
|
|
103
|
+
|
|
104
|
+
for (const raw of lines) {
|
|
105
|
+
let entry: Record<string, unknown>;
|
|
106
|
+
try {
|
|
107
|
+
entry = JSON.parse(raw) as Record<string, unknown>;
|
|
108
|
+
} catch {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const u = userText(entry);
|
|
113
|
+
if (u !== null) {
|
|
114
|
+
// A background-task notification occupies the user role but is
|
|
115
|
+
// not a chat message — the exact shape that made the language
|
|
116
|
+
// defect invisible. It must never move the pin.
|
|
117
|
+
if (!isInjectedBody(u)) {
|
|
118
|
+
const verdict = classify(u);
|
|
119
|
+
if (verdict.language !== 'und') pinned = verdict.language;
|
|
120
|
+
}
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const a = assistantText(entry);
|
|
125
|
+
if (a === null || a.trim() === '') continue;
|
|
126
|
+
c.assistant_turns += 1;
|
|
127
|
+
|
|
128
|
+
if (detectPromissory(a) !== null) c.promissory_fires += 1;
|
|
129
|
+
if (detectLanguage(a, pinned) !== null) {
|
|
130
|
+
c.language_fires += 1;
|
|
131
|
+
myLanguage += 1;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const agree = Math.min(myLanguage, scannerLanguage);
|
|
136
|
+
c.language_agree += agree;
|
|
137
|
+
c.language_only += Math.max(0, myLanguage - scannerLanguage);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return c;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function render(c: Counts): string {
|
|
144
|
+
const pct = (n: number): string =>
|
|
145
|
+
c.assistant_turns === 0 ? '0.0' : ((100 * n) / c.assistant_turns).toFixed(1);
|
|
146
|
+
return [
|
|
147
|
+
`measure-turn-end-gate · ${c.sessions} sessions · ${c.assistant_turns} assistant turns`,
|
|
148
|
+
'',
|
|
149
|
+
` detector A (promissory) fires on ${c.promissory_fires} turns (${pct(c.promissory_fires)}%)`,
|
|
150
|
+
` detector B (language) fires on ${c.language_fires} turns (${pct(c.language_fires)}%)`,
|
|
151
|
+
` of which the independent scanner also flagged: ${c.language_agree}`,
|
|
152
|
+
` fired where the scanner saw nothing: ${c.language_only}`,
|
|
153
|
+
'',
|
|
154
|
+
' The second line is the one that matters for a BLOCKING guard: those are',
|
|
155
|
+
' the turns this detector would have refused and the other instrument would',
|
|
156
|
+
' not. They are candidate false positives, not confirmed ones — the two',
|
|
157
|
+
' instruments read different spans (first prose line vs. whole stripped',
|
|
158
|
+
' reply), so disagreement is expected in both directions.',
|
|
159
|
+
'',
|
|
160
|
+
' No recall figure for detector A: promissory closings are not machine-',
|
|
161
|
+
' scanned anywhere in this tree, so there is no ground truth to score.',
|
|
162
|
+
].join('\n');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function main(argv: string[] = process.argv.slice(2)): number {
|
|
166
|
+
let store = '';
|
|
167
|
+
let limit = 30;
|
|
168
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
169
|
+
if (argv[i] === '--store' && argv[i + 1] !== undefined) {
|
|
170
|
+
store = argv[i + 1]!;
|
|
171
|
+
i += 1;
|
|
172
|
+
} else if (argv[i] === '--limit' && argv[i + 1] !== undefined) {
|
|
173
|
+
limit = parseInt(argv[i + 1]!, 10);
|
|
174
|
+
i += 1;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (!store) {
|
|
178
|
+
process.stderr.write('measure-turn-end-gate: --store <transcript dir> is required\n');
|
|
179
|
+
return 2;
|
|
180
|
+
}
|
|
181
|
+
const counts = measure(store, limit);
|
|
182
|
+
if (counts.sessions === 0) {
|
|
183
|
+
// A measurement over zero sessions that exits 0 is the repo's own
|
|
184
|
+
// "gates that scan nothing" failure. Refuse instead.
|
|
185
|
+
process.stderr.write(`measure-turn-end-gate: no .jsonl sessions under ${store}\n`);
|
|
186
|
+
return 2;
|
|
187
|
+
}
|
|
188
|
+
process.stdout.write(`${render(counts)}\n`);
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (process.argv[1] !== undefined && process.argv[1].includes('measure_turn_end_gate')) {
|
|
193
|
+
process.exit(main());
|
|
194
|
+
}
|
|
@@ -13,9 +13,12 @@
|
|
|
13
13
|
* [--tiers sonnet,opus] [--agent-combo implementer,judge] [--wall-clock-ms 18500] [--dispatch-outcome DONE] \
|
|
14
14
|
* [--first-pass-success true|false] [--escalated true|false] \
|
|
15
15
|
* [--init-tokens 1200] [--payload-hash <hex8-64>] [--lookup-class definition|references|string-existence|report-run] \
|
|
16
|
-
* [--route-taken primitive|subagent] [--budget-hit true|false] [--correctness-match true|false] \
|
|
16
|
+
* [--route-taken primitive|subagent|ask] [--budget-hit true|false] [--correctness-match true|false] \
|
|
17
17
|
* [--cache-hit true|false] [--origin lean-init-2026] \
|
|
18
18
|
* [--rules-carried 32] [--rules-used 5] \
|
|
19
|
+
* [--work-tokens 41000] [--floor-provenance measured|estimated] \
|
|
20
|
+
* [--capsule-emitted true|false] [--capsule-entries 9] \
|
|
21
|
+
* [--watermark-step 8] [--saturation-step 4] [--trigger-arm-earlier watermark|saturation|tie] \
|
|
19
22
|
* [--dir <audit-dir>] [--dry-run]
|
|
20
23
|
*
|
|
21
24
|
* Read by `src/scripts/orchestration_savings_report.ts`.
|
|
@@ -39,6 +42,7 @@ import {
|
|
|
39
42
|
type LineOutcome,
|
|
40
43
|
type RecordInput,
|
|
41
44
|
type RouteTaken,
|
|
45
|
+
type TriggerArm,
|
|
42
46
|
} from './_lib/orchestration_record.js';
|
|
43
47
|
|
|
44
48
|
// Exported so other writers of the same audit-log-v1 orchestration line
|
|
@@ -141,6 +145,13 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
141
145
|
origin: str(flags, 'origin'),
|
|
142
146
|
rules_carried: int(flags, 'rules-carried'),
|
|
143
147
|
rules_used: int(flags, 'rules-used'),
|
|
148
|
+
work_tokens: int(flags, 'work-tokens'),
|
|
149
|
+
floor_provenance: str(flags, 'floor-provenance') as Provenance | undefined,
|
|
150
|
+
capsule_emitted: bool(flags, 'capsule-emitted'),
|
|
151
|
+
capsule_entries: int(flags, 'capsule-entries'),
|
|
152
|
+
watermark_step: int(flags, 'watermark-step'),
|
|
153
|
+
saturation_step: int(flags, 'saturation-step'),
|
|
154
|
+
trigger_arm_earlier: str(flags, 'trigger-arm-earlier') as TriggerArm | undefined,
|
|
144
155
|
phase: str(flags, 'phase') as LinePhase | undefined,
|
|
145
156
|
outcome: str(flags, 'outcome') as LineOutcome | undefined,
|
|
146
157
|
confidence_band: str(flags, 'confidence-band') as Band | undefined,
|