@event4u/agent-config 16.1.0 → 16.2.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 +237 -248
- package/dist/agent-src/commands/roadmap/process-full.md +1 -1
- package/dist/agent-src/commands/work.md +4 -0
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +4 -8
- package/dist/agent-src/rules/ui-audit-gate.md +1 -2
- package/dist/agent-src/skills/accessibility-auditor/data/aria-patterns.csv +1 -1
- package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +2 -2
- package/dist/agent-src/skills/design-review/SKILL.md +30 -4
- package/dist/agent-src/skills/design-review/references/verification-automation.md +32 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +16 -1
- package/dist/agent-src/skills/existing-ui-audit/references/component-taxonomy-detection.md +56 -0
- package/dist/agent-src/skills/markitdown/SKILL.md +4 -0
- package/dist/agent-src/skills/playwright-testing/SKILL.md +30 -6
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +23 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +30 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
- package/dist/agent-src/templates/scripts/work_engine/state.ts +57 -7
- package/dist/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
- package/dist/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
- package/dist/cli/python/knowledge_ingest.js +1 -1
- package/dist/cli/python/workspace_drive.js +1 -1
- package/dist/cli-delegate/{chunk-S3VFXMTM.js → chunk-4XFP2Y3E.js} +1 -1
- package/dist/cli-delegate/{chunk-HDVVGPQW.js → chunk-7XGBX6ZH.js} +471 -387
- package/dist/cli-delegate/{chunk-GJCBB2UG.js → chunk-FP36SWJD.js} +432 -144
- package/dist/cli-delegate/{chunk-UNCRJBT3.js → chunk-IGD6ERX6.js} +31 -0
- package/dist/cli-delegate/{chunk-FXYUYU4N.js → chunk-JNMVLWLR.js} +42 -1
- package/dist/cli-delegate/{chunk-QJWAUMJW.js → chunk-YJ72UIAK.js} +730 -15
- package/dist/cli-delegate/cmd_conformance.js +902 -782
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +4 -4
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +10 -10
- 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 +6666 -2581
- package/dist/hooks/dispatch.sha256 +1 -0
- package/dist/install/install.mjs +848 -733
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +2747 -1510
- package/dist/server/schemas/settings.js +23 -0
- package/dist/ui/assets/index-Cwq4d0hX.js +23 -0
- package/dist/ui/assets/index-Cyjy8bWO.css +1 -0
- package/dist/ui/index.html +2 -2
- package/docs/contracts/settings-classes.md +6 -4
- package/package.json +4 -4
- package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
- package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
- package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
- package/src/agent-src/templates/scripts/work_engine/state.ts +57 -7
- package/src/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
- package/src/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
- package/src/config/agent-settings.template.yml +29 -0
- package/src/config/ci-local-parity.yml +18 -0
- package/src/config/continuity-surface.json +2 -2
- package/src/config/gate-coverage.yml +234 -3
- package/src/config/gate-violation-baselines.json +7 -3
- package/src/config/hook-latency-budget.json +21 -0
- package/src/config/hook-token-budget.json +2 -2
- package/src/config/metric-registry.yml +114 -0
- package/src/config/placeholder-drift-budget.json +10 -0
- package/src/config/routing-coverage-seed.json +18 -4
- package/src/scripts/_cli/cmd_doctor.ts +5 -19
- package/src/scripts/_cli/doctor_execution.ts +100 -17
- package/src/scripts/_cli/doctor_network_posture.ts +226 -0
- package/src/scripts/_lib/body_portable.ts +102 -0
- package/src/scripts/_lib/cc_transcript.ts +132 -1
- package/src/scripts/_lib/concern_sla_window.ts +200 -0
- package/src/scripts/_lib/forge_protection.ts +114 -0
- package/src/scripts/_lib/forge_reader.ts +558 -0
- package/src/scripts/_lib/host_env_write.ts +111 -0
- package/src/scripts/_lib/llm_proposer_transport.ts +14 -4
- package/src/scripts/_lib/obligations.ts +119 -8
- package/src/scripts/_lib/probe_inputs.ts +271 -0
- package/src/scripts/_lib/read_surface_scan.ts +322 -0
- package/src/scripts/_lib/release_highlights.ts +2 -1
- package/src/scripts/_lib/retrieval_sanitize.ts +77 -16
- package/src/scripts/_lib/shell_write_shapes.ts +108 -0
- package/src/scripts/_lib/structural_hiding.ts +622 -0
- package/src/scripts/_lib/turn_end_refusals.ts +525 -4
- package/src/scripts/_lib/turn_end_transcript.ts +216 -0
- package/src/scripts/_lib/verification_command.ts +324 -0
- package/src/scripts/_lib/verification_evidence.ts +647 -0
- package/src/scripts/_lib/verify_clause.ts +280 -0
- package/src/scripts/ai_team/team_dispatch.ts +4 -0
- package/src/scripts/before_complete_hook.ts +340 -23
- package/src/scripts/bench_hook_latency.ts +412 -5
- package/src/scripts/cache_realization_report.ts +61 -1
- package/src/scripts/check_argument_hint.ts +287 -0
- package/src/scripts/check_claims.ts +125 -1
- package/src/scripts/check_enforcement_coverage.ts +38 -9
- package/src/scripts/check_enforcement_matrix.ts +245 -3
- package/src/scripts/check_finding_dispositions.ts +22 -3
- package/src/scripts/check_generator_sync.ts +71 -0
- package/src/scripts/check_host_docs_digest.ts +786 -0
- package/src/scripts/check_host_format_column.ts +559 -0
- package/src/scripts/check_input_references.ts +228 -0
- package/src/scripts/check_inputs_parity.ts +235 -0
- package/src/scripts/check_kernel_edit_ratified.ts +47 -5
- package/src/scripts/check_kill_switch_table.ts +294 -0
- package/src/scripts/check_placeholder_drift.ts +221 -0
- package/src/scripts/check_read_surface_coverage.ts +515 -0
- package/src/scripts/check_release_holds.ts +2 -1
- package/src/scripts/check_routing_coverage.ts +378 -11
- package/src/scripts/check_static_parity.ts +17 -0
- package/src/scripts/ci_settle.ts +145 -19
- package/src/scripts/closure_scan.ts +115 -1
- package/src/scripts/compile_hook_manifest.ts +60 -16
- package/src/scripts/external_sources_denylist.json +29 -4
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +42 -5
- package/src/scripts/hooks/block_config_weakening.ts +229 -4
- package/src/scripts/hooks/block_kernel_rule_writes.ts +9 -78
- package/src/scripts/hooks/block_plumbing_writes.ts +314 -0
- package/src/scripts/hooks/bundle_integrity.ts +291 -0
- package/src/scripts/hooks/chain_nudge_hook.ts +1 -2
- package/src/scripts/hooks/code_graph_context_hook.ts +1 -2
- package/src/scripts/hooks/comment_discipline_hook.ts +1 -3
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/concern_timings.ts +94 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +1 -2
- package/src/scripts/hooks/design_pass_hook.ts +100 -9
- package/src/scripts/hooks/design_slop_hook.ts +1 -2
- package/src/scripts/hooks/dispatch_hook.ts +25 -19
- package/src/scripts/hooks/edit_shape_hook.ts +1 -2
- package/src/scripts/hooks/end_review_nudge_hook.ts +1 -1
- package/src/scripts/hooks/evidence_independence.ts +1 -2
- package/src/scripts/hooks/exit_codes.ts +128 -0
- package/src/scripts/hooks/host_lowering.json +1 -0
- package/src/scripts/hooks/host_lowering.ts +128 -3
- package/src/scripts/hooks/host_lowering.yaml +262 -46
- package/src/scripts/hooks/host_semantics.ts +30 -2
- package/src/scripts/hooks/injection_budget.ts +9 -2
- package/src/scripts/hooks/interruption_ledger_hook.ts +1 -2
- package/src/scripts/hooks/journal_record_hook.ts +1 -2
- package/src/scripts/hooks/memory_recall_hook.ts +1 -2
- package/src/scripts/hooks/obligation_settle_hook.ts +151 -5
- package/src/scripts/hooks/one_question_per_ask_hook.ts +25 -4
- package/src/scripts/hooks/orchestration_record_hook.ts +1 -2
- package/src/scripts/hooks/permission_gate.ts +1 -2
- package/src/scripts/hooks/push_settle_hook.ts +1 -3
- package/src/scripts/hooks/replay_hook.ts +2 -1
- package/src/scripts/hooks/reread_guard_hook.ts +1 -2
- package/src/scripts/hooks/review_baseline_hook.ts +1 -2
- package/src/scripts/hooks/rtk_wrap_hook.ts +1 -2
- package/src/scripts/hooks/rule_inject_hook.ts +8 -5
- package/src/scripts/hooks/run_continuation_hook.ts +13 -15
- package/src/scripts/hooks/skill_route_hook.ts +1 -2
- package/src/scripts/hooks/source_first_gate_hook.ts +1 -2
- package/src/scripts/hooks/spawn_guard_shadow_hook.ts +1 -2
- package/src/scripts/hooks/subagent_ledger_hook.ts +1 -2
- package/src/scripts/hooks/suggestion_capture_hook.ts +1 -1
- package/src/scripts/hooks/table_fingerprint.ts +49 -0
- package/src/scripts/hooks/telemetry_flush_hook.ts +1 -2
- package/src/scripts/hooks/telemetry_usage_hook.ts +1 -2
- package/src/scripts/hooks/tool_result_bytes_hook.ts +93 -9
- package/src/scripts/hooks/turn_end_gate_hook.ts +337 -324
- package/src/scripts/hooks/ui_route_nudge_hook.ts +1 -2
- package/src/scripts/install.ts +3 -4
- package/src/scripts/lint_deny_text.ts +419 -0
- package/src/scripts/lint_exit_codes.ts +262 -0
- package/src/scripts/lint_hook_manifest.ts +55 -0
- package/src/scripts/lint_roadmap_blockers.ts +78 -12
- package/src/scripts/lint_skill_descriptions.ts +1 -1
- package/src/scripts/lint_skill_scripts_readonly.ts +1 -1
- package/src/scripts/mcp_server/prompts.ts +142 -1
- package/src/scripts/measure_skill_ranker_baseline.ts +305 -51
- package/src/scripts/measure_turn_end_gate.ts +193 -0
- package/src/scripts/memory_lookup.ts +1 -1
- package/src/scripts/prepack-check.mjs +53 -0
- package/src/scripts/release_publication.ts +13 -1
- package/src/scripts/report_invocation_surface.ts +383 -0
- package/src/scripts/report_obligation_carriers.ts +699 -0
- package/src/scripts/report_obligation_writer_split.ts +236 -0
- package/src/scripts/report_skill_menu_census.ts +208 -0
- package/src/scripts/report_standing_payload_by_host.ts +1 -1
- package/src/scripts/roadmap_verify_share.ts +213 -0
- package/src/scripts/schemas/command.schema.json +44 -0
- package/src/scripts/schemas/review-findings.schema.json +4 -0
- package/src/scripts/schemas/skill.schema.json +44 -0
- package/src/scripts/self_review_gate.ts +359 -10
- package/src/scripts/semantic_parity_census.ts +314 -0
- package/src/scripts/session_eol_report.ts +54 -1
- package/src/scripts/sweep_dead_scan_roots.ts +1 -1
- package/src/scripts/trigger_eval_grandfather.json +0 -2
- package/src/scripts/trigger_eval_rotation.ts +122 -27
- package/src/scripts/ui_conformance_probe.ts +156 -4
- package/src/scripts/update_prices.ts +85 -3
- package/src/scripts/write_bundle_digest.mjs +74 -0
- package/src/server/schemas/settings.ts +31 -0
- package/src/templates/consumer-settings/ONBOARDING.md +1 -0
- package/dist/ui/assets/index-DH1xc62v.js +0 -29
- package/dist/ui/assets/index-jSfMW8_t.css +0 -1
- /package/dist/cli-delegate/{chunk-YUM5KURG.js → chunk-7RQP4UDH.js} +0 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* report_obligation_writer_split — which kind of tree wrote the obligation rows.
|
|
4
|
+
*
|
|
5
|
+
* WHAT QUESTION THIS ANSWERS, AND WHY IT NEEDED A TOOL.
|
|
6
|
+
* `obligation-settle-shadow-bar` is pre-registered against the shadow rows under
|
|
7
|
+
* `agents/runtime/state/obligations/`, and its own clause (7) says that corpus is
|
|
8
|
+
* one machine's runtime state. Until the rows carried a writer, that sentence was
|
|
9
|
+
* the ONLY place the fact lived: a reader holding the ledgers could count rows and
|
|
10
|
+
* could not separate them, so a reading that cleared the floor would have been
|
|
11
|
+
* quotable as evidence about a population nobody had measured. This prints the
|
|
12
|
+
* separation, from the rows themselves.
|
|
13
|
+
*
|
|
14
|
+
* IT REACHES NO VERDICT, AND THAT IS THE DESIGN. A split is an input to the corpus
|
|
15
|
+
* decision held in `road-to-an-obligation-row-that-names-its-writer`, not the
|
|
16
|
+
* decision — the three options there (accept the one-machine corpus and amend the
|
|
17
|
+
* bar, widen it deliberately, or file the window `resolved-null`) are owner-
|
|
18
|
+
* reserved, and all three name arming as owner-reserved too. So this exits 0 on
|
|
19
|
+
* every path, prints counts, and says nothing about whether anything may be armed.
|
|
20
|
+
* A reporter that graded the corpus would be handing the arming discussion a
|
|
21
|
+
* verdict wearing a number's clothes.
|
|
22
|
+
*
|
|
23
|
+
* `absent` IS REPORTED APART FROM `unknown`, and the distinction is load-bearing.
|
|
24
|
+
* `unknown` is a row whose writer WAS resolved and came back unresolvable; `absent`
|
|
25
|
+
* is a row written before the field existed. Folding them together would let the
|
|
26
|
+
* whole pre-field corpus read as "we looked and could not tell", which is a
|
|
27
|
+
* stronger claim than "we had not started looking". The ledger's own `parseRows`
|
|
28
|
+
* maps both to `unknown` — correct for a consumer that only needs a role — so this
|
|
29
|
+
* reads the JSON directly rather than through it, and uses `isWriterRole` for the
|
|
30
|
+
* vocabulary so the two cannot drift.
|
|
31
|
+
*
|
|
32
|
+
* PRIVACY, at the precision the code holds. Output is counts, plus the SCAN ROOT,
|
|
33
|
+
* which the caller supplied and which is the one thing a reader needs in order to
|
|
34
|
+
* know what was measured. Nothing else crosses: no session id, no ledger filename,
|
|
35
|
+
* no rule id, no timestamp. The scan reads those and keeps none of them.
|
|
36
|
+
*
|
|
37
|
+
* WRITES NOTHING. It opens files for reading and creates no directory, not even
|
|
38
|
+
* the ledger directory it looks for. A missing directory is a reported state, not
|
|
39
|
+
* a thing to fix.
|
|
40
|
+
*
|
|
41
|
+
* Exit: 0 always, except a usage error (1). Advisory — it gates nothing.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import * as fs from 'node:fs';
|
|
45
|
+
import * as path from 'node:path';
|
|
46
|
+
import process from 'node:process';
|
|
47
|
+
import { fileURLToPath } from 'node:url';
|
|
48
|
+
|
|
49
|
+
import { OBLIGATION_STATE_DIR, WRITER_ROLES, isWriterRole } from './_lib/obligations.js';
|
|
50
|
+
|
|
51
|
+
/** The reported classes: the closed vocabulary, plus the pre-field case. */
|
|
52
|
+
export const REPORTED_CLASSES = [...WRITER_ROLES, 'absent'] as const;
|
|
53
|
+
export type ReportedClass = (typeof REPORTED_CLASSES)[number];
|
|
54
|
+
|
|
55
|
+
/** The three row arrays a ledger carries, named as the file names them. */
|
|
56
|
+
export const ROW_ARRAYS = ['delivered', 'discharged', 'shadow'] as const;
|
|
57
|
+
export type RowArray = (typeof ROW_ARRAYS)[number];
|
|
58
|
+
|
|
59
|
+
export type Tally = Record<ReportedClass, number>;
|
|
60
|
+
|
|
61
|
+
export interface WriterSplit {
|
|
62
|
+
/** The directory scanned, echoed so a reader knows what was measured. */
|
|
63
|
+
readonly store: string;
|
|
64
|
+
/** `false` when the ledger directory does not exist at all. */
|
|
65
|
+
readonly storeExists: boolean;
|
|
66
|
+
/** Ledger files opened and parsed. */
|
|
67
|
+
readonly ledgers: number;
|
|
68
|
+
/** Files present but unreadable or unparseable — reported, never silently dropped. */
|
|
69
|
+
readonly unreadable: number;
|
|
70
|
+
/** Per row array, the split by writer class. */
|
|
71
|
+
readonly byArray: Record<RowArray, Tally>;
|
|
72
|
+
/** Every array folded together. */
|
|
73
|
+
readonly total: Tally;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function emptyTally(): Tally {
|
|
77
|
+
return { package: 0, consumer: 0, unknown: 0, absent: 0 };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Classify one row's writer field.
|
|
82
|
+
*
|
|
83
|
+
* A field that is missing entirely is `absent`. A field that is present but
|
|
84
|
+
* outside the closed vocabulary is `unknown`, matching `parseRows`: a corrupt
|
|
85
|
+
* provenance label degrades, it never deletes the record of a real row.
|
|
86
|
+
*/
|
|
87
|
+
export function classifyRow(row: unknown): ReportedClass {
|
|
88
|
+
if (typeof row !== 'object' || row === null) return 'absent';
|
|
89
|
+
if (!('writer' in row)) return 'absent';
|
|
90
|
+
const writer = (row as { writer: unknown }).writer;
|
|
91
|
+
return isWriterRole(writer) ? writer : 'unknown';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Tally one ledger's arrays into an accumulator. */
|
|
95
|
+
function tallyLedger(doc: unknown, into: Record<RowArray, Tally>): void {
|
|
96
|
+
if (typeof doc !== 'object' || doc === null) return;
|
|
97
|
+
for (const name of ROW_ARRAYS) {
|
|
98
|
+
const rows = (doc as Record<string, unknown>)[name];
|
|
99
|
+
if (!Array.isArray(rows)) continue;
|
|
100
|
+
for (const row of rows) into[name][classifyRow(row)] += 1;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Read every ledger under `root` and split its rows by writer class.
|
|
106
|
+
*
|
|
107
|
+
* Opens files for reading only. A file that will not parse increments
|
|
108
|
+
* `unreadable` rather than being skipped in silence — a corpus with unreadable
|
|
109
|
+
* members is a different corpus from a clean one, and a reader deciding whether
|
|
110
|
+
* a sample floor is met needs to know which they have.
|
|
111
|
+
*/
|
|
112
|
+
export function readWriterSplit(root: string): WriterSplit {
|
|
113
|
+
const store = path.join(root, OBLIGATION_STATE_DIR);
|
|
114
|
+
const byArray: Record<RowArray, Tally> = {
|
|
115
|
+
delivered: emptyTally(),
|
|
116
|
+
discharged: emptyTally(),
|
|
117
|
+
shadow: emptyTally(),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
let entries: string[];
|
|
121
|
+
try {
|
|
122
|
+
entries = fs.readdirSync(store).filter((f) => f.endsWith('.json'));
|
|
123
|
+
} catch {
|
|
124
|
+
return { store, storeExists: false, ledgers: 0, unreadable: 0, byArray, total: emptyTally() };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let ledgers = 0;
|
|
128
|
+
let unreadable = 0;
|
|
129
|
+
for (const entry of entries) {
|
|
130
|
+
let doc: unknown;
|
|
131
|
+
try {
|
|
132
|
+
doc = JSON.parse(fs.readFileSync(path.join(store, entry), 'utf-8'));
|
|
133
|
+
} catch {
|
|
134
|
+
unreadable += 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
ledgers += 1;
|
|
138
|
+
tallyLedger(doc, byArray);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const total = emptyTally();
|
|
142
|
+
for (const name of ROW_ARRAYS) {
|
|
143
|
+
for (const cls of REPORTED_CLASSES) total[cls] += byArray[name][cls];
|
|
144
|
+
}
|
|
145
|
+
return { store, storeExists: true, ledgers, unreadable, byArray, total };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const sum = (t: Tally): number => REPORTED_CLASSES.reduce((n, c) => n + t[c], 0);
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Render the split as text.
|
|
152
|
+
*
|
|
153
|
+
* The line a reader is most likely to quote is the shadow row, so it is named
|
|
154
|
+
* rather than left to be inferred from a table: the pre-registered bar counts
|
|
155
|
+
* shadow rows and nothing else, and a split over `delivered` answers a question
|
|
156
|
+
* the bar did not ask.
|
|
157
|
+
*/
|
|
158
|
+
export function render(split: WriterSplit): string {
|
|
159
|
+
const out: string[] = [];
|
|
160
|
+
out.push(`scanned: ${split.store}`);
|
|
161
|
+
if (!split.storeExists) {
|
|
162
|
+
out.push(' the ledger directory does not exist — nothing was measured.');
|
|
163
|
+
out.push(' This is not a reading of zero. It is the absence of a corpus.');
|
|
164
|
+
return `${out.join('\n')}\n`;
|
|
165
|
+
}
|
|
166
|
+
out.push(` ledgers: ${split.ledgers}${split.unreadable > 0 ? `, unreadable: ${split.unreadable}` : ''}`);
|
|
167
|
+
if (split.ledgers === 0) {
|
|
168
|
+
out.push(' no ledger files — the directory exists and holds nothing.');
|
|
169
|
+
return `${out.join('\n')}\n`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const width = Math.max(...ROW_ARRAYS.map((n) => n.length));
|
|
173
|
+
out.push('');
|
|
174
|
+
out.push(`${'rows'.padEnd(width)} ${REPORTED_CLASSES.map((c) => c.padStart(9)).join('')} total`);
|
|
175
|
+
for (const name of ROW_ARRAYS) {
|
|
176
|
+
const t = split.byArray[name];
|
|
177
|
+
const cells = REPORTED_CLASSES.map((c) => String(t[c]).padStart(9)).join('');
|
|
178
|
+
out.push(`${name.padEnd(width)} ${cells} ${String(sum(t)).padStart(7)}`);
|
|
179
|
+
}
|
|
180
|
+
const cells = REPORTED_CLASSES.map((c) => String(split.total[c]).padStart(9)).join('');
|
|
181
|
+
out.push(`${'all'.padEnd(width)} ${cells} ${String(sum(split.total)).padStart(7)}`);
|
|
182
|
+
|
|
183
|
+
out.push('');
|
|
184
|
+
out.push(
|
|
185
|
+
`shadow rows — the only array the pre-registered bar counts: ${sum(split.byArray.shadow)}`,
|
|
186
|
+
);
|
|
187
|
+
out.push(
|
|
188
|
+
'`absent` is a row written before the writer field existed; `unknown` is a row whose',
|
|
189
|
+
);
|
|
190
|
+
out.push('writer was resolved and came back unresolvable. They are not the same reading.');
|
|
191
|
+
out.push('');
|
|
192
|
+
out.push('This is a split, not a verdict. Whether the corpus is fit to read the bar off,');
|
|
193
|
+
out.push('and what follows for the detector, are owner decisions this tool does not make.');
|
|
194
|
+
return `${out.join('\n')}\n`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Read one flag's value, refusing a flag whose value is missing.
|
|
199
|
+
*
|
|
200
|
+
* A flag present as the last argv element must be a usage error and never a
|
|
201
|
+
* silent fall back to the default. The same slip in a sibling reporter measured
|
|
202
|
+
* the DEFAULT corpus while printing what read as a settled number — a confident
|
|
203
|
+
* figure from a tree nobody asked for is worse than no figure at all.
|
|
204
|
+
*/
|
|
205
|
+
function argValue(argv: string[], flag: string, fallback: string): string {
|
|
206
|
+
const i = argv.indexOf(flag);
|
|
207
|
+
if (i === -1) return fallback;
|
|
208
|
+
const value = argv[i + 1];
|
|
209
|
+
if (value === undefined || value.startsWith('--')) {
|
|
210
|
+
process.stderr.write(`report_obligation_writer_split: ${flag} needs a value\n`);
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
return value;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function main(argv: string[]): number {
|
|
217
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
218
|
+
process.stdout.write(
|
|
219
|
+
'usage: report_obligation_writer_split [--root <dir>] [--json]\n\n' +
|
|
220
|
+
' --root <dir> tree whose agents/runtime/state/obligations/ to read (default: cwd)\n' +
|
|
221
|
+
' --json emit the split as JSON instead of text\n\n' +
|
|
222
|
+
'Read-only. Writes nothing, gates nothing, exits 0.\n',
|
|
223
|
+
);
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
|
226
|
+
const root = argValue(argv, '--root', process.cwd());
|
|
227
|
+
const split = readWriterSplit(root);
|
|
228
|
+
process.stdout.write(argv.includes('--json') ? `${JSON.stringify(split, null, 2)}\n` : render(split));
|
|
229
|
+
return 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
233
|
+
|
|
234
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(_HERE)) {
|
|
235
|
+
process.exit(main(process.argv.slice(2)));
|
|
236
|
+
}
|
|
@@ -53,8 +53,21 @@
|
|
|
53
53
|
* `command-only` and `flow-only` name a CANDIDATE for `user-invocable: false`,
|
|
54
54
|
* never a decision — see the sole-entry limit above.
|
|
55
55
|
*
|
|
56
|
+
* THE PER-PROFILE MENU, added by `road-to-a-menu-whose-precision-is-measured`
|
|
57
|
+
* 4.1. `--profile minimal|balanced|full|all` prints the menu byte count each
|
|
58
|
+
* shipped install profile delivers. Each profile's preset is read from its own
|
|
59
|
+
* file, and the answer is that all three are equal: no shipped preset declares
|
|
60
|
+
* a key that selects skills, so each one ships the whole menu.
|
|
61
|
+
*
|
|
62
|
+
* The equality note is WITHHELD when any profile carries a key listed in
|
|
63
|
+
* {@link SKILL_SELECTING_INI_KEYS}, and the key is named instead. That is the
|
|
64
|
+
* whole falsifiability claim, stated no wider than it holds: the byte count
|
|
65
|
+
* would not move — nothing here knows what such a key means — but the report
|
|
66
|
+
* stops asserting a conclusion whose premise has changed.
|
|
67
|
+
*
|
|
56
68
|
* Usage:
|
|
57
69
|
* ./scripts-run src/scripts/report_skill_menu_census # stdout summary
|
|
70
|
+
* ./scripts-run src/scripts/report_skill_menu_census --profile all
|
|
58
71
|
* ./scripts-run src/scripts/report_skill_menu_census --emit # write the artifact
|
|
59
72
|
* ./scripts-run src/scripts/report_skill_menu_census --emit --pin <sha>
|
|
60
73
|
*
|
|
@@ -313,8 +326,203 @@ function headSha(root: string): string {
|
|
|
313
326
|
return execFileSync('git', ['-C', root, 'rev-parse', 'HEAD'], { encoding: 'utf-8' }).trim();
|
|
314
327
|
}
|
|
315
328
|
|
|
329
|
+
// ── per-profile menu bytes (road-to-a-menu-whose-precision-is-measured 4.1) ──
|
|
330
|
+
|
|
331
|
+
/** The shipped install presets, mirroring `install.ts` SUPPORTED_PROFILES. */
|
|
332
|
+
export const PROFILES = ['minimal', 'balanced', 'full'] as const;
|
|
333
|
+
export type Profile = (typeof PROFILES)[number];
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Preset keys that would narrow the skill menu.
|
|
337
|
+
*
|
|
338
|
+
* EMPTY, and the emptiness is the finding rather than a stub. A preset selects
|
|
339
|
+
* `rule_loading_tier`, `discipline_profile` and `chat_history_frequency`; which
|
|
340
|
+
* SKILLS are written is decided by `packs:` frontmatter against the active pack
|
|
341
|
+
* set, which no preset names.
|
|
342
|
+
*
|
|
343
|
+
* It is a TRIPWIRE, not a selector, and the distinction is worth stating
|
|
344
|
+
* plainly because the first version of this comment overclaimed it. Nothing
|
|
345
|
+
* here knows what such a key would MEAN, so nothing here can apply one: the
|
|
346
|
+
* byte count would not move. What the list buys is that a preset carrying a
|
|
347
|
+
* listed key is NAMED in the output and the equality note is withheld — so the
|
|
348
|
+
* report stops asserting a conclusion whose premise has changed, instead of
|
|
349
|
+
* printing the same prose over a different tree.
|
|
350
|
+
*/
|
|
351
|
+
export const SKILL_SELECTING_INI_KEYS: readonly string[] = [];
|
|
352
|
+
|
|
353
|
+
/** Flat `key=value` read of a profile preset; `;` comments and blanks dropped. */
|
|
354
|
+
export function readProfileIni(root: string, profile: Profile): Record<string, string> {
|
|
355
|
+
const p = path.join(root, 'src', 'config', 'profiles', `${profile}.ini`);
|
|
356
|
+
const out: Record<string, string> = {};
|
|
357
|
+
for (const raw of fs.readFileSync(p, 'utf-8').split('\n')) {
|
|
358
|
+
const line = raw.trim();
|
|
359
|
+
if (line === '' || line.startsWith(';') || line.startsWith('#') || line.startsWith('[')) continue;
|
|
360
|
+
const eq = line.indexOf('=');
|
|
361
|
+
if (eq === -1) continue;
|
|
362
|
+
out[line.slice(0, eq).trim()] = line.slice(eq + 1).trim();
|
|
363
|
+
}
|
|
364
|
+
return out;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export interface ProfileMenu {
|
|
368
|
+
readonly profile: Profile;
|
|
369
|
+
/** Skills this profile writes onto the model menu. */
|
|
370
|
+
readonly menuSkills: number;
|
|
371
|
+
/** Bytes of `- <name>: <description>\n`, the shape `censusSkillsCatalog` sums. */
|
|
372
|
+
readonly menuBytes: number;
|
|
373
|
+
/** chars/4, the estimator the payload budget uses. */
|
|
374
|
+
readonly tokensEstimate: number;
|
|
375
|
+
/** `declared_skill_catalogue_tokens` from the preset, or null when absent. */
|
|
376
|
+
readonly declaredCatalogueTokens: number | null;
|
|
377
|
+
/** Preset keys found that would narrow the menu. Empty today, by measurement. */
|
|
378
|
+
readonly selectingKeys: readonly string[];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* One profile's menu, computed from its preset plus the skill tree.
|
|
383
|
+
*
|
|
384
|
+
* The catalogue LINE is the same shape `preamble_byte_census.censusSkillsCatalog`
|
|
385
|
+
* sums — `- <name>: <description>\n`. The POPULATION deliberately is not, and the
|
|
386
|
+
* difference is stated rather than smoothed over: that census sums every
|
|
387
|
+
* `SKILL.md`, because the payload bucket it feeds is about what the host lists;
|
|
388
|
+
* this one drops a skill carrying `user-invocable: false` or
|
|
389
|
+
* `disable-model-invocation: true`, because a menu is what the MODEL may pick
|
|
390
|
+
* from. Two skills carry such a flag today, so over the same tree and the same
|
|
391
|
+
* line shape this report reads 311 B below the full-population sum. Expect the
|
|
392
|
+
* two to differ; a reader who finds them equal has found a defect in one.
|
|
393
|
+
*/
|
|
394
|
+
export function menuForProfile(root: string, profile: Profile): ProfileMenu {
|
|
395
|
+
const ini = readProfileIni(root, profile);
|
|
396
|
+
const selectingKeys = SKILL_SELECTING_INI_KEYS.filter((k) => k in ini);
|
|
397
|
+
const skillsDir = path.join(root, 'src', 'skills');
|
|
398
|
+
let menuSkills = 0;
|
|
399
|
+
let menuBytes = 0;
|
|
400
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
401
|
+
if (!entry.isDirectory()) continue;
|
|
402
|
+
const skillMd = path.join(skillsDir, entry.name, 'SKILL.md');
|
|
403
|
+
if (!fs.existsSync(skillMd)) continue;
|
|
404
|
+
const text = fs.readFileSync(skillMd, 'utf-8');
|
|
405
|
+
const flags = menuFlags(text);
|
|
406
|
+
if (flags.userInvocable === false || flags.disableModel === true) continue;
|
|
407
|
+
const name = /^name:\s*(\S.*?)\s*$/mu.exec(text.slice(0, 4000))?.[1] ?? entry.name;
|
|
408
|
+
const description = frontmatterDescription(text);
|
|
409
|
+
menuSkills += 1;
|
|
410
|
+
menuBytes += `- ${name}: ${description}\n`.length;
|
|
411
|
+
}
|
|
412
|
+
const declared = ini['declared_skill_catalogue_tokens'];
|
|
413
|
+
return {
|
|
414
|
+
profile,
|
|
415
|
+
menuSkills,
|
|
416
|
+
menuBytes,
|
|
417
|
+
tokensEstimate: Math.round(menuBytes / 4),
|
|
418
|
+
declaredCatalogueTokens: declared === undefined ? null : Number(declared),
|
|
419
|
+
selectingKeys,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* The `description:` value, folded lines included.
|
|
425
|
+
*
|
|
426
|
+
* A `[^\n]+` capture was measured 17.1 % low elsewhere in this tree because it
|
|
427
|
+
* truncates every folded multi-line description; this reads to the next
|
|
428
|
+
* top-level key instead.
|
|
429
|
+
*
|
|
430
|
+
* A double-quoted value has its escapes DECODED. Leaving them encoded counted
|
|
431
|
+
* `\"` as two characters and over-reported the menu by 56 B across 13 skills —
|
|
432
|
+
* small, but the same class of defect as the 17.1 % one above, and a function
|
|
433
|
+
* whose docstring cites that fix should not carry a second instance of it.
|
|
434
|
+
*/
|
|
435
|
+
export function frontmatterDescription(text: string): string {
|
|
436
|
+
if (!text.startsWith('---')) return '';
|
|
437
|
+
const end = text.indexOf('\n---', 3);
|
|
438
|
+
if (end === -1) return '';
|
|
439
|
+
const lines = text.slice(3, end).split('\n');
|
|
440
|
+
const at = lines.findIndex((l) => /^description:/u.test(l));
|
|
441
|
+
if (at === -1) return '';
|
|
442
|
+
const parts = [(lines[at] as string).replace(/^description:\s*/u, '')];
|
|
443
|
+
for (let i = at + 1; i < lines.length; i += 1) {
|
|
444
|
+
const l = lines[i] as string;
|
|
445
|
+
if (/^\S/u.test(l)) break;
|
|
446
|
+
parts.push(l.trim());
|
|
447
|
+
}
|
|
448
|
+
const joined = parts.join(' ').trim();
|
|
449
|
+
if (joined.length >= 2 && joined.startsWith('"') && joined.endsWith('"')) {
|
|
450
|
+
return joined.slice(1, -1).replace(/\\(["\\])/gu, '$1');
|
|
451
|
+
}
|
|
452
|
+
if (joined.length >= 2 && joined.startsWith("'") && joined.endsWith("'")) {
|
|
453
|
+
return joined.slice(1, -1).replace(/''/gu, "'");
|
|
454
|
+
}
|
|
455
|
+
return joined;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** Render the per-profile menu table. Returns the lines, so tests read them. */
|
|
459
|
+
export function renderProfileMenus(menus: readonly ProfileMenu[]): string[] {
|
|
460
|
+
const L: string[] = [];
|
|
461
|
+
L.push('menu bytes per install profile (name + description, the catalogue shape):');
|
|
462
|
+
L.push(' profile skills menu_bytes ~tokens declared');
|
|
463
|
+
for (const m of menus) {
|
|
464
|
+
L.push(
|
|
465
|
+
` ${m.profile.padEnd(10)} ${String(m.menuSkills).padStart(6)} ` +
|
|
466
|
+
`${String(m.menuBytes).padStart(12)} ${String(m.tokensEstimate).padStart(9)} ` +
|
|
467
|
+
`${(m.declaredCatalogueTokens === null ? '—' : String(m.declaredCatalogueTokens)).padStart(10)}`,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
const flagged = menus.filter((m) => m.selectingKeys.length > 0);
|
|
471
|
+
if (flagged.length > 0) {
|
|
472
|
+
L.push('');
|
|
473
|
+
L.push(' A preset declares a key that could select skills, so the equality below is NOT');
|
|
474
|
+
L.push(' asserted — the premise it rested on has changed. Keys found:');
|
|
475
|
+
for (const m of flagged) L.push(` ${m.profile}: ${m.selectingKeys.join(', ')}`);
|
|
476
|
+
return L;
|
|
477
|
+
}
|
|
478
|
+
const distinct = new Set(menus.map((m) => m.menuBytes));
|
|
479
|
+
if (menus.length > 1 && distinct.size === 1) {
|
|
480
|
+
L.push('');
|
|
481
|
+
L.push(
|
|
482
|
+
' All profiles report the SAME menu_bytes, and that is a measurement, not a',
|
|
483
|
+
);
|
|
484
|
+
L.push(
|
|
485
|
+
' placeholder: no preset declares a key that selects skills, so each ships the',
|
|
486
|
+
);
|
|
487
|
+
L.push(
|
|
488
|
+
' whole menu. The preset chooses which RULES activate; which SKILLS are written',
|
|
489
|
+
);
|
|
490
|
+
L.push(
|
|
491
|
+
' is decided by `packs:` frontmatter against the active pack set. A menu-bytes',
|
|
492
|
+
);
|
|
493
|
+
L.push(
|
|
494
|
+
' lever therefore has to move packs or the menu flags — changing profile does',
|
|
495
|
+
);
|
|
496
|
+
L.push(' nothing. The three presets already record this in their own comments.');
|
|
497
|
+
}
|
|
498
|
+
return L;
|
|
499
|
+
}
|
|
500
|
+
|
|
316
501
|
export function main(argv: readonly string[]): number {
|
|
317
502
|
const root = REPO_ROOT;
|
|
503
|
+
const profIdx = argv.indexOf('--profile');
|
|
504
|
+
if (profIdx !== -1) {
|
|
505
|
+
const want = argv[profIdx + 1] ?? 'all';
|
|
506
|
+
const chosen: Profile[] =
|
|
507
|
+
want === 'all' ? [...PROFILES] : PROFILES.includes(want as Profile) ? [want as Profile] : [];
|
|
508
|
+
if (chosen.length === 0) {
|
|
509
|
+
process.stderr.write(
|
|
510
|
+
`report_skill_menu_census: --profile expects ${PROFILES.join(' | ')} | all, got ${want}\n`,
|
|
511
|
+
);
|
|
512
|
+
return 2;
|
|
513
|
+
}
|
|
514
|
+
if (argv.includes('--emit')) {
|
|
515
|
+
// Refused rather than ignored: `--emit` writes the census artifact,
|
|
516
|
+
// `--profile` prints a table and writes nothing, and silently doing
|
|
517
|
+
// the second when the caller asked for both is how an artifact goes
|
|
518
|
+
// un-refreshed without anyone noticing.
|
|
519
|
+
process.stderr.write('report_skill_menu_census: --profile and --emit are separate runs\n');
|
|
520
|
+
return 2;
|
|
521
|
+
}
|
|
522
|
+
const menus = chosen.map((p) => menuForProfile(root, p));
|
|
523
|
+
process.stdout.write(`${renderProfileMenus(menus).join('\n')}\n`);
|
|
524
|
+
return 0;
|
|
525
|
+
}
|
|
318
526
|
const emit = argv.includes('--emit');
|
|
319
527
|
const pinIdx = argv.indexOf('--pin');
|
|
320
528
|
const pin = pinIdx !== -1 && pinIdx + 1 < argv.length ? (argv[pinIdx + 1] as string) : headSha(root);
|
|
@@ -156,7 +156,7 @@ export const HOST_SURFACES: readonly HostSurface[] = [
|
|
|
156
156
|
},
|
|
157
157
|
{
|
|
158
158
|
host: 'codex', surface: '.codex/agent-config.md', perRuleTree: false,
|
|
159
|
-
writer: 'src/scripts/install.ts:
|
|
159
|
+
writer: 'src/scripts/install.ts:1431',
|
|
160
160
|
// Scoped to `.codex` rather than the bare filename: nine other lines in
|
|
161
161
|
// `install.ts` write an `agent-config.md` for some other host, so the
|
|
162
162
|
// unscoped anchor would have accepted a drift onto any of them — the
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* The runnable share of `verify:` clauses, per roadmap.
|
|
4
|
+
*
|
|
5
|
+
* A `verify:` clause names a command and never names what the command must
|
|
6
|
+
* produce, so a step can be flipped on a command that cannot fail. Two numbers
|
|
7
|
+
* say how bad that is, and neither had a producing command before this script:
|
|
8
|
+
* how many clauses name a COMMAND at all (the runnable share), and how many
|
|
9
|
+
* name an EXPECTATION (the falsifiable share).
|
|
10
|
+
*
|
|
11
|
+
* THIS IS A READER, NOT A GATE. It exits 0 on every corpus, including one where
|
|
12
|
+
* both shares are zero. That is a property of the reader, not a description of
|
|
13
|
+
* the tree: measured 2026-09-29, the tree carried 230 clauses, 93 naming a
|
|
14
|
+
* command and 6 naming an expectation — neither share is zero, and the
|
|
15
|
+
* commissioning roadmap's claim that the second one was is the first thing this
|
|
16
|
+
* script refuted. A ratchet needs a false-positive rate and two readings a
|
|
17
|
+
* quarter apart; it has neither, and a gate that reds all but a handful of
|
|
18
|
+
* roadmaps at once gets weakened until it finds nothing. What it buys instead
|
|
19
|
+
* is that the NEXT reading is a delta rather than a re-derivation.
|
|
20
|
+
*
|
|
21
|
+
* THE UNIT, stated because the figure is meaningless without it. One unit is
|
|
22
|
+
* one STEP BLOCK (`- [ ]`, `- [x]`, `- [~]`, `- [-]`, plus its continuation
|
|
23
|
+
* lines) that carries a `verify:` clause. Not one line: a clause wraps, and a
|
|
24
|
+
* line count would double-count a wrapped clause and count prose that merely
|
|
25
|
+
* mentions the token. Step blocks come from `closure_scan`'s own `units()`, so
|
|
26
|
+
* the two instruments agree on what a step is; that also inherits its
|
|
27
|
+
* exclusions — fenced code, and the `## Decisions` / `## Blockers` /
|
|
28
|
+
* `## Kill register` / `## Risk Register` discharge sections, where a mention
|
|
29
|
+
* of `verify:` is commentary rather than a step's oracle.
|
|
30
|
+
*
|
|
31
|
+
* Closed steps are counted. A `[x]` flipped on a clause that could not fail is
|
|
32
|
+
* the defect in its completed form, and excluding them would report the
|
|
33
|
+
* backlog as smaller every time someone closed one.
|
|
34
|
+
*
|
|
35
|
+
* CLI:
|
|
36
|
+
* ./scripts-run src/scripts/roadmap_verify_share
|
|
37
|
+
* ./scripts-run src/scripts/roadmap_verify_share --json
|
|
38
|
+
* ./scripts-run src/scripts/roadmap_verify_share --root <dir> [--recursive]
|
|
39
|
+
*
|
|
40
|
+
* Exit codes: 0 always, except 2 for bad argv or a dead scan root.
|
|
41
|
+
*/
|
|
42
|
+
import * as fs from 'node:fs';
|
|
43
|
+
import * as path from 'node:path';
|
|
44
|
+
import process from 'node:process';
|
|
45
|
+
import { fileURLToPath } from 'node:url';
|
|
46
|
+
|
|
47
|
+
import { units } from './closure_scan.js';
|
|
48
|
+
import { DeadScopeError, assertScanned } from './_lib/scan_scope.js';
|
|
49
|
+
import { parseVerifyClause } from './_lib/verify_clause.js';
|
|
50
|
+
|
|
51
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
52
|
+
const _PROG = 'roadmap_verify_share';
|
|
53
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
54
|
+
const DEFAULT_ROOT = 'agents/roadmaps';
|
|
55
|
+
|
|
56
|
+
export interface FileShare {
|
|
57
|
+
readonly file: string;
|
|
58
|
+
/** Step blocks carrying a `verify:` clause. */
|
|
59
|
+
readonly clauses: number;
|
|
60
|
+
/** Of those, the ones naming a command — the runnable share. */
|
|
61
|
+
readonly withCommand: number;
|
|
62
|
+
/** Of those, the ones naming an expectation — the falsifiable share. */
|
|
63
|
+
readonly withExpectation: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ShareReport {
|
|
67
|
+
readonly root: string;
|
|
68
|
+
readonly files: number;
|
|
69
|
+
readonly clauses: number;
|
|
70
|
+
readonly withCommand: number;
|
|
71
|
+
readonly withExpectation: number;
|
|
72
|
+
readonly perFile: readonly FileShare[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Count the clauses in one roadmap's text. */
|
|
76
|
+
export function shareOf(file: string, text: string): FileShare {
|
|
77
|
+
let clauses = 0;
|
|
78
|
+
let withCommand = 0;
|
|
79
|
+
let withExpectation = 0;
|
|
80
|
+
for (const u of units(text.split('\n'))) {
|
|
81
|
+
if (!u.hasVerify) continue;
|
|
82
|
+
const clause = parseVerifyClause(u.blockText);
|
|
83
|
+
if (clause === null) continue;
|
|
84
|
+
clauses += 1;
|
|
85
|
+
if (clause.command !== null) withCommand += 1;
|
|
86
|
+
if (clause.expect !== null) withExpectation += 1;
|
|
87
|
+
}
|
|
88
|
+
return { file, clauses, withCommand, withExpectation };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function markdownFiles(root: string, recursive: boolean): string[] {
|
|
92
|
+
const out: string[] = [];
|
|
93
|
+
const walk = (dir: string): void => {
|
|
94
|
+
let entries: fs.Dirent[];
|
|
95
|
+
try {
|
|
96
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
97
|
+
} catch {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
for (const e of entries) {
|
|
101
|
+
const full = path.join(dir, e.name);
|
|
102
|
+
if (e.isDirectory()) {
|
|
103
|
+
if (recursive) walk(full);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (e.name.endsWith('.md')) out.push(full);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
walk(root);
|
|
110
|
+
return out.sort();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function report(rootAbs: string, rootLabel: string, recursive: boolean): ShareReport {
|
|
114
|
+
const files = markdownFiles(rootAbs, recursive);
|
|
115
|
+
// A root that resolved to nothing is blindness, not cleanliness: the share
|
|
116
|
+
// would read as a clean 0/0 and the reader would believe it.
|
|
117
|
+
assertScanned({ gate: _PROG, scanned: files.length, units: 'roadmap(s)', roots: [rootLabel] });
|
|
118
|
+
const perFile = files
|
|
119
|
+
.map((f) => shareOf(path.relative(REPO_ROOT, f), fs.readFileSync(f, 'utf-8')))
|
|
120
|
+
.filter((s) => s.clauses > 0);
|
|
121
|
+
return {
|
|
122
|
+
root: rootLabel,
|
|
123
|
+
files: files.length,
|
|
124
|
+
clauses: perFile.reduce((n, s) => n + s.clauses, 0),
|
|
125
|
+
withCommand: perFile.reduce((n, s) => n + s.withCommand, 0),
|
|
126
|
+
withExpectation: perFile.reduce((n, s) => n + s.withExpectation, 0),
|
|
127
|
+
perFile,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function pct(n: number, of: number): string {
|
|
132
|
+
return of === 0 ? 'n/a' : `${((n / of) * 100).toFixed(1)}%`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function render(r: ShareReport): string {
|
|
136
|
+
const lines: string[] = [];
|
|
137
|
+
lines.push(`root: ${r.root}`);
|
|
138
|
+
lines.push(`roadmaps scanned: ${String(r.files)}`);
|
|
139
|
+
lines.push(`verify clauses: ${String(r.clauses)}`);
|
|
140
|
+
lines.push(` naming a command: ${String(r.withCommand)} (${pct(r.withCommand, r.clauses)})`);
|
|
141
|
+
lines.push(` naming an expectation: ${String(r.withExpectation)} (${pct(r.withExpectation, r.clauses)})`);
|
|
142
|
+
lines.push('');
|
|
143
|
+
lines.push('| roadmap | clauses | command | expectation |');
|
|
144
|
+
lines.push('|---|---:|---:|---:|');
|
|
145
|
+
for (const s of r.perFile) {
|
|
146
|
+
lines.push(
|
|
147
|
+
`| \`${path.basename(s.file)}\` | ${String(s.clauses)} | ${String(s.withCommand)} | ${String(s.withExpectation)} |`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return lines.join('\n') + '\n';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function _usage(): string {
|
|
154
|
+
return (
|
|
155
|
+
`usage: ${_PROG} [-h] [--json] [--root <dir>] [--recursive]\n\n` +
|
|
156
|
+
`Report the runnable and falsifiable share of \`verify:\` clauses. Gates nothing.\n`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function main(argv?: readonly string[]): number {
|
|
161
|
+
const args = argv ?? process.argv.slice(2);
|
|
162
|
+
let json = false;
|
|
163
|
+
let recursive = false;
|
|
164
|
+
let root = DEFAULT_ROOT;
|
|
165
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
166
|
+
const a = args[i] as string;
|
|
167
|
+
if (a === '-h' || a === '--help') {
|
|
168
|
+
process.stdout.write(_usage());
|
|
169
|
+
return 0;
|
|
170
|
+
} else if (a === '--json') json = true;
|
|
171
|
+
else if (a === '--recursive') recursive = true;
|
|
172
|
+
else if (a === '--root') {
|
|
173
|
+
const next = args[i + 1];
|
|
174
|
+
if (next === undefined) {
|
|
175
|
+
process.stderr.write(`${_usage()}${_PROG}: error: --root needs a directory\n`);
|
|
176
|
+
return 2;
|
|
177
|
+
}
|
|
178
|
+
root = next;
|
|
179
|
+
i += 1;
|
|
180
|
+
} else {
|
|
181
|
+
process.stderr.write(`${_usage()}${_PROG}: error: unrecognized arguments: ${a}\n`);
|
|
182
|
+
return 2;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const rootAbs = path.isAbsolute(root) ? root : path.join(REPO_ROOT, root);
|
|
187
|
+
let r: ShareReport;
|
|
188
|
+
try {
|
|
189
|
+
r = report(rootAbs, root, recursive);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
if (e instanceof DeadScopeError) {
|
|
192
|
+
process.stderr.write(`${e.message}\n`);
|
|
193
|
+
return 2;
|
|
194
|
+
}
|
|
195
|
+
throw e;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
process.stdout.write(json ? JSON.stringify(r, null, 2) + '\n' : render(r));
|
|
199
|
+
return 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function _isCliEntry(): boolean {
|
|
203
|
+
if (!process.argv[1]) return false;
|
|
204
|
+
try {
|
|
205
|
+
return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
206
|
+
} catch {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
212
|
+
process.exitCode = main();
|
|
213
|
+
}
|