@event4u/agent-config 15.0.0 → 16.1.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 +301 -347
- package/CONTRIBUTING.md +9 -0
- package/README.md +2 -2
- package/dist/agent-src/commands/analyze/inbox.md +26 -0
- package/dist/agent-src/commands/analyze/roadmap-repos.md +27 -0
- package/dist/agent-src/commands/challenge-me/closure.md +178 -0
- package/dist/agent-src/commands/challenge-me.md +9 -2
- package/dist/agent-src/commands/council/default.md +25 -5
- package/dist/agent-src/commands/feature/plan.md +28 -1
- package/dist/agent-src/commands/feature/roadmap.md +28 -1
- package/dist/agent-src/commands/implement-ticket.md +27 -0
- package/dist/agent-src/commands/jira-ticket.md +27 -0
- package/dist/agent-src/commands/pr/create.md +29 -0
- package/dist/agent-src/commands/pr/merge.md +60 -9
- package/dist/agent-src/commands/roadmap/ai-council.md +7 -1
- package/dist/agent-src/commands/roadmap/create.md +77 -29
- package/dist/agent-src/commands/roadmap/materialize.md +27 -0
- package/dist/agent-src/commands/roadmap/process-full.md +123 -18
- package/dist/agent-src/commands/team.md +22 -0
- package/dist/agent-src/contexts/authority/scope-growth-ownership.md +80 -0
- package/dist/agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +97 -0
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +44 -3
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +15 -15
- package/dist/agent-src/contexts/execution/decision-closure-execution.md +93 -0
- package/dist/agent-src/contexts/execution/fix-loop-ladder.md +108 -0
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +4 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +75 -6
- package/dist/agent-src/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/dist/agent-src/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/dist/agent-src/guidelines/design-fidelity-routing.md +29 -0
- package/dist/agent-src/rules/autonomous-execution.md +15 -8
- package/dist/agent-src/rules/delegation-policy.md +23 -45
- package/dist/agent-src/rules/design-fidelity.md +32 -37
- package/dist/agent-src/rules/evaluator-independence.md +43 -49
- package/dist/agent-src/rules/test-first.md +41 -0
- package/dist/agent-src/rules/user-interaction.md +31 -30
- package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
- package/dist/agent-src/skills/ai-council/SKILL.md +11 -3
- package/dist/agent-src/skills/api-design/data/manifest.json +3 -3
- package/dist/agent-src/skills/database/data/manifest.json +4 -4
- package/dist/agent-src/skills/database/data/query-tuning.csv +3 -3
- package/dist/agent-src/skills/design-review/SKILL.md +50 -0
- package/dist/agent-src/skills/error-handling-patterns/SKILL.md +9 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +14 -4
- package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/storybook-workshop/SKILL.md +42 -3
- package/dist/agent-src/skills/threat-modeling/SKILL.md +9 -4
- package/dist/agent-src/skills/threat-modeling/data/manifest.json +4 -4
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +37 -3
- package/dist/agent-src/templates/agent-settings.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +188 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
- package/dist/cli-delegate/check_update_banner.js +40 -2
- package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
- package/dist/cli-delegate/chunk-5R7HOLAF.js +99 -0
- package/dist/cli-delegate/{chunk-IVBPRCQO.js → chunk-6KFEC3G7.js} +1 -1
- package/dist/cli-delegate/{chunk-2TAQ2K4V.js → chunk-6KM6ORRY.js} +55 -386
- package/dist/cli-delegate/{chunk-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
- package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-FXYUYU4N.js} +57 -13
- package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-GJCBB2UG.js} +168 -10
- package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-HDVVGPQW.js} +873 -539
- package/dist/cli-delegate/chunk-NUHLI2RX.js +80 -0
- package/dist/cli-delegate/{chunk-HZ52BRGG.js → chunk-P6ZHP5W2.js} +15 -90
- package/dist/cli-delegate/{chunk-TYSY33MD.js → chunk-QJWAUMJW.js} +12 -2
- package/dist/cli-delegate/{chunk-57PN4GYJ.js → chunk-RGNIWTEA.js} +1 -1
- package/dist/cli-delegate/chunk-ROYYTEZU.js +39 -0
- package/dist/cli-delegate/{chunk-2KKXJW25.js → chunk-S3VFXMTM.js} +1 -1
- package/dist/cli-delegate/{chunk-3FKR64R5.js → chunk-UNCRJBT3.js} +31 -7
- package/dist/cli-delegate/{chunk-2DK2VJWG.js → chunk-WGXTVKCQ.js} +20 -6
- package/dist/cli-delegate/chunk-YDPILCOL.js +152 -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 +918 -325
- package/dist/cli-delegate/cmd_doctor.js +7 -5
- package/dist/cli-delegate/cmd_explain.js +5 -3
- package/dist/cli-delegate/cmd_export.js +9 -7
- package/dist/cli-delegate/cmd_fleet.js +4 -2
- package/dist/cli-delegate/cmd_handoff.js +6 -6
- 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 +3 -2
- package/dist/cli-delegate/cmd_preflight.js +4 -2
- package/dist/cli-delegate/cmd_prune.js +10 -8
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +4 -4
- package/dist/cli-delegate/cmd_settings_get.js +28 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +9 -7
- package/dist/cli-delegate/cmd_settings_set.js +14 -1
- package/dist/cli-delegate/cmd_sync.js +10 -8
- package/dist/cli-delegate/cmd_uninstall.js +16 -13
- 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 +12 -10
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +87 -40
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +10 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +8 -6
- package/dist/hooks/dispatch.js +2601 -1073
- package/dist/install/claudeRuleRewrite.js +14 -3
- package/dist/install/conflict.js +60 -10
- package/dist/install/conflictTracker.js +218 -0
- package/dist/install/fsPrimitives.js +54 -0
- package/dist/install/install.mjs +2008 -1481
- package/dist/install/manifestFiles.js +259 -0
- package/dist/install/preflight.js +14 -2
- package/dist/install/preserve.js +157 -0
- package/dist/install/recordedOwnership.js +132 -0
- package/dist/install/txlog.js +11 -2
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1612 -667
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +3 -2
- package/dist/scripts/_lib/fs_atomic.js +167 -0
- package/dist/scripts/_lib/installed_lock.js +1 -1
- package/dist/scripts/_lib/installed_tools.js +460 -0
- package/dist/scripts/_lib/lean_projection_mode.js +3 -2
- package/dist/scripts/_lib/settings_renamed_keys.js +78 -0
- package/dist/server/io/yamlIO.js +50 -0
- package/dist/server/routes/install.js +8 -0
- package/dist/server/routes/wizard.js +9 -3
- package/dist/server/schemas/settings.js +17 -5
- package/dist/shared/missionExecution.js +97 -0
- package/dist/ui/assets/{index-D0jaCRPM.js → index-DH1xc62v.js} +17 -17
- package/dist/ui/index.html +1 -1
- package/docs/MIGRATION.md +103 -0
- package/docs/contracts/settings-classes.md +26 -7
- package/docs/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/docs/guidelines/design-fidelity-routing.md +29 -0
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
- package/src/config/agent-settings.template.yml +139 -27
- package/src/config/auto-rule-budget.json +16 -0
- package/src/config/ci-local-parity.yml +18 -5
- package/src/config/continuity-surface.json +28 -0
- package/src/config/gate-coverage.yml +146 -13
- package/src/config/gate-violation-baselines.json +6 -6
- package/src/config/hook-token-budget.json +2 -0
- package/src/config/host-injection-effect.json +1 -1
- package/src/config/loop-surfaces.yaml +215 -0
- package/src/config/platform-anchor.json +3 -2
- package/src/config/preamble-payload-budget.json +12 -7
- package/src/config/preamble-payload-exceptions.json +28 -0
- package/src/config/release-gate-locality.yml +11 -0
- package/src/config/routing-coverage-seed.json +18 -4
- package/src/config/rule-enforcement-baseline.json +0 -1
- package/src/install/claudeRuleRewrite.ts +16 -2
- package/src/install/conflict.ts +65 -9
- package/src/install/conflictTracker.ts +299 -0
- package/src/install/fsPrimitives.ts +54 -0
- package/src/install/manifestFiles.ts +312 -0
- package/src/install/preflight.ts +16 -2
- package/src/install/preserve.ts +205 -0
- package/src/install/recordedOwnership.ts +149 -0
- package/src/install/txlog.ts +11 -2
- package/src/install/types.ts +19 -4
- package/src/scripts/_cli/cmd_conformance.ts +88 -15
- package/src/scripts/_cli/cmd_doctor.ts +4 -3
- package/src/scripts/_cli/cmd_route_explain.ts +9 -2
- package/src/scripts/_cli/cmd_settings_get.ts +44 -0
- package/src/scripts/_cli/doctor_execution.ts +90 -0
- package/src/scripts/_dispatch.bash +3 -1
- package/src/scripts/_lib/activation_payload.ts +247 -0
- package/src/scripts/_lib/agent_settings.ts +4 -2
- package/src/scripts/_lib/authority_path.ts +102 -0
- package/src/scripts/_lib/base_ref_payload.ts +315 -0
- package/src/scripts/_lib/cascade_base.ts +94 -0
- package/src/scripts/_lib/changelog_eras.ts +7 -2
- package/src/scripts/_lib/continuation_ladder.ts +149 -2
- package/src/scripts/_lib/council_transport.ts +129 -0
- package/src/scripts/_lib/delivery_ready.ts +112 -0
- package/src/scripts/_lib/dropped_decision.ts +142 -0
- package/src/scripts/_lib/forge_protection.ts +267 -0
- package/src/scripts/_lib/hook_platform_keys.ts +27 -0
- package/src/scripts/_lib/installed_lock.ts +1 -1
- package/src/scripts/_lib/json_python_parity.ts +127 -0
- package/src/scripts/_lib/lean_projection_mode.ts +14 -3
- package/src/scripts/_lib/loop_surfaces.ts +382 -0
- package/src/scripts/_lib/measured_payload_ceiling.ts +424 -0
- package/src/scripts/_lib/mission_record.ts +190 -0
- package/src/scripts/_lib/obligation_frequency.ts +217 -0
- package/src/scripts/_lib/obligations.ts +402 -0
- package/src/scripts/_lib/orchestration_record.ts +11 -20
- package/src/scripts/_lib/payload_catalogue_completeness.ts +211 -0
- package/src/scripts/_lib/release_findings_ingest.ts +324 -0
- package/src/scripts/_lib/release_highlights.ts +199 -10
- package/src/scripts/_lib/release_holds.ts +493 -0
- package/src/scripts/_lib/release_migration_gate.ts +184 -0
- package/src/scripts/_lib/release_tag_dispatch.ts +63 -0
- package/src/scripts/_lib/review_baseline.ts +254 -0
- package/src/scripts/_lib/review_skipped_record.ts +61 -1
- package/src/scripts/_lib/rides_along.ts +102 -0
- package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
- package/src/scripts/_lib/standing_bound_ratchet.ts +216 -103
- package/src/scripts/_lib/test_delta.ts +166 -0
- package/src/scripts/_lib/test_provenance.ts +171 -0
- package/src/scripts/_lib/turn_end_refusals.ts +51 -4
- package/src/scripts/_lib/typed_op_grant.ts +246 -0
- package/src/scripts/_lib/typed_op_watch.ts +185 -0
- package/src/scripts/ai_council/config.ts +8 -11
- package/src/scripts/ai_council/fallback_config.ts +5 -3
- package/src/scripts/ai_council/ownership_classes.ts +118 -0
- package/src/scripts/ask_block_census.ts +285 -15
- package/src/scripts/check_beta_review_markers.ts +35 -4
- package/src/scripts/check_enforcement_matrix.ts +624 -0
- package/src/scripts/check_finding_dispositions.ts +167 -16
- package/src/scripts/check_gate_reachability.ts +173 -10
- package/src/scripts/check_generator_sync.ts +954 -0
- package/src/scripts/check_held_object_arrivals.ts +381 -0
- package/src/scripts/check_preamble_payload_budget.ts +474 -124
- package/src/scripts/check_release_highlights.ts +33 -1
- package/src/scripts/check_release_holds.ts +567 -0
- package/src/scripts/check_release_published.ts +1 -1
- package/src/scripts/check_reply_consistency.ts +86 -17
- package/src/scripts/check_rule_projection_integrity.ts +22 -12
- package/src/scripts/check_test_delta.ts +294 -0
- package/src/scripts/check_test_weakening.ts +299 -0
- package/src/scripts/closure_scan.ts +333 -0
- package/src/scripts/compile_hook_manifest.ts +43 -3
- package/src/scripts/council_record_shape.ts +209 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +127 -12
- package/src/scripts/hooks/block_kernel_rule_writes.ts +12 -2
- package/src/scripts/hooks/concern_registry.ts +6 -0
- package/src/scripts/hooks/design_pass_hook.ts +128 -3
- package/src/scripts/hooks/end_review_nudge_hook.ts +101 -21
- package/src/scripts/hooks/evidence_independence.ts +96 -0
- package/src/scripts/hooks/memory_recall_hook.ts +540 -0
- package/src/scripts/hooks/obligation_settle_hook.ts +182 -0
- package/src/scripts/hooks/review_baseline_hook.ts +130 -0
- package/src/scripts/hooks/rule_inject_hook.ts +49 -1
- package/src/scripts/hooks/run_continuation_hook.ts +17 -20
- package/src/scripts/hooks/turn_end_gate_hook.ts +232 -14
- package/src/scripts/hooks_doctor.ts +89 -1
- package/src/scripts/hooks_status.ts +19 -0
- package/src/scripts/install.ts +107 -207
- package/src/scripts/interruption_report.ts +8 -2
- package/src/scripts/janitor.ts +15 -1
- package/src/scripts/lint_decision_classes.ts +464 -0
- package/src/scripts/lint_hook_manifest.ts +11 -0
- package/src/scripts/lint_major_migration_sections.ts +450 -0
- package/src/scripts/lint_plan_risk_register.ts +47 -3
- package/src/scripts/lint_roadmap_blockers.ts +61 -0
- package/src/scripts/lint_roadmap_producers.ts +202 -0
- package/src/scripts/measure_rule_budget.ts +101 -5
- package/src/scripts/measure_turn_end_gate.ts +302 -5
- package/src/scripts/new_roadmap.ts +37 -0
- package/src/scripts/orchestration_record.ts +0 -5
- package/src/scripts/release.ts +66 -162
- package/src/scripts/release_drill.ts +460 -15
- package/src/scripts/release_env.ts +1 -1
- package/src/scripts/release_publication.ts +52 -1
- package/src/scripts/report_enforcement_drift.ts +566 -0
- package/src/scripts/report_held_object_arrivals.ts +456 -0
- package/src/scripts/report_host_injection_effect.ts +3 -11
- package/src/scripts/report_standing_payload_by_host.ts +26 -152
- package/src/scripts/schemas/agent-settings.schema.json +23 -0
- package/src/scripts/schemas/command.schema.json +4 -0
- package/src/scripts/scope_growth.ts +219 -0
- package/src/scripts/sync_agent_settings.ts +140 -18
- package/src/scripts/trigger_eval_grandfather.json +0 -1
- package/src/scripts/ui_conformance_probe.ts +622 -0
- package/src/server/io/yamlIO.ts +48 -0
- package/src/server/schemas/settings.ts +31 -7
- package/src/shared/missionExecution.ts +126 -0
- package/dist/cli-delegate/chunk-523HCFYO.js +0 -90
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runtime activation payload — what a trigger match charges per fire.
|
|
3
|
+
*
|
|
4
|
+
* The third of the three numbers `road-to-the-delivery-flip-that-tells-the-truth`
|
|
5
|
+
* Phase 3 separates. The other two already have names and a record: ADR-270
|
|
6
|
+
* calls them `source_corpus` (what the source tree could deliver) and
|
|
7
|
+
* `host_payload` (what one host actually receives at rest), and requires them to
|
|
8
|
+
* be structurally separate fields because "until the two are named apart, every
|
|
9
|
+
* ceiling discussion risks comparing unlike quantities". After a per-host
|
|
10
|
+
* projection flip a third quantity diverges from both: the bytes a delivery
|
|
11
|
+
* concern injects when a rule's trigger fires. A standing reduction published
|
|
12
|
+
* without it is a saving published without its cost.
|
|
13
|
+
*
|
|
14
|
+
* MEASURED THROUGH THE CONCERN'S OWN SELECTION, never a second model of it.
|
|
15
|
+
* `matchTierRules` + `selectForInjection` are the functions the runtime concern
|
|
16
|
+
* calls; an offline figure computed by a different matcher would price a set the
|
|
17
|
+
* concern does not deliver. That is the same single-definition rule
|
|
18
|
+
* `_lib/rule_injection.ts` was written for, applied one layer out.
|
|
19
|
+
*
|
|
20
|
+
* THE CAP IS A PARAMETER, not an import. The value lives on the concern
|
|
21
|
+
* (`hooks/rule_inject_hook.ts::CAP_BYTES`) and every caller passes it, so this
|
|
22
|
+
* module stays free of the hook's import closure and can be read from a gate, a
|
|
23
|
+
* report, or a bundled path without dragging a hook into it.
|
|
24
|
+
*
|
|
25
|
+
* Moved here from `report_standing_payload_by_host.ts`, which had the only
|
|
26
|
+
* implementation and is still its largest caller — same move, and for the same
|
|
27
|
+
* reason, as `censusRuleDir` into `_lib/carrier_divergence.ts`: a second reader
|
|
28
|
+
* needed it and a second copy is how two numbers start disagreeing.
|
|
29
|
+
*/
|
|
30
|
+
import * as fs from 'node:fs';
|
|
31
|
+
import * as path from 'node:path';
|
|
32
|
+
|
|
33
|
+
import { loadRouter, matchTierRules, selectForInjection } from './rule_injection.js';
|
|
34
|
+
|
|
35
|
+
/** The frozen corpus every fire figure in this module is sampled from. */
|
|
36
|
+
export const ROUTING_MATRIX_REL = path.join('tests', 'eval', 'routing-matrix');
|
|
37
|
+
|
|
38
|
+
/** One positive prompt of the frozen corpus, with the open files it declares. */
|
|
39
|
+
export interface CorpusPrompt {
|
|
40
|
+
prompt: string;
|
|
41
|
+
openFiles: string[] | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Every YAML shape inside `positives:` this hand parser cannot read.
|
|
46
|
+
*
|
|
47
|
+
* `readCorpusPositives` accepts only `- prompt: "…"` with a double-quoted
|
|
48
|
+
* same-line scalar and an inline `open_files: [...]`. Any other legal shape —
|
|
49
|
+
* single quotes, an unquoted scalar, a folded block, a block sequence for
|
|
50
|
+
* `open_files` — is SILENTLY SKIPPED, which drops fires out of the distribution
|
|
51
|
+
* a budget row is derived from. A measurement that governs a budget must fail
|
|
52
|
+
* loudly on a shape it cannot read rather than under-count, so callers refuse on
|
|
53
|
+
* a non-empty result here.
|
|
54
|
+
*
|
|
55
|
+
* Reported as a shape problem rather than fixed by widening the parser: this
|
|
56
|
+
* module must not grow a second YAML implementation, and the corpus is frozen,
|
|
57
|
+
* so the honest answer to an unreadable shape is to say which file carries it.
|
|
58
|
+
*/
|
|
59
|
+
export function corpusShapeProblems(root: string): string[] {
|
|
60
|
+
const dir = path.join(root, ROUTING_MATRIX_REL);
|
|
61
|
+
const problems: string[] = [];
|
|
62
|
+
if (!fs.existsSync(dir)) return problems;
|
|
63
|
+
for (const f of fs.readdirSync(dir).filter((n) => n.endsWith('.yaml')).sort()) {
|
|
64
|
+
let inPositives = false;
|
|
65
|
+
let lineNo = 0;
|
|
66
|
+
for (const raw of fs.readFileSync(path.join(dir, f), 'utf-8').split('\n')) {
|
|
67
|
+
lineNo += 1;
|
|
68
|
+
if (/^positives:/u.test(raw)) { inPositives = true; continue; }
|
|
69
|
+
if (/^near_misses:/u.test(raw)) { inPositives = false; continue; }
|
|
70
|
+
if (!inPositives) continue;
|
|
71
|
+
if (/^\s*-\s*prompt:/u.test(raw) && !/^\s*-\s*prompt:\s*"(.*)"\s*$/u.test(raw)) {
|
|
72
|
+
problems.push(
|
|
73
|
+
`${f}:${String(lineNo)}: a \`- prompt:\` this parser cannot read — only a ` +
|
|
74
|
+
`double-quoted same-line scalar is accepted: ${JSON.stringify(raw.trim())}`,
|
|
75
|
+
);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (/^\s*-\s*/u.test(raw) && !/^\s*-\s*(prompt|command|open_files):/u.test(raw)
|
|
79
|
+
&& raw.trim() !== '-' && raw.trim() !== '') {
|
|
80
|
+
// A bare sequence item under `positives:` is a shape with no
|
|
81
|
+
// `prompt:` key on its first line — folded or nested.
|
|
82
|
+
if (!/^\s{4,}/u.test(raw)) {
|
|
83
|
+
problems.push(
|
|
84
|
+
`${f}:${String(lineNo)}: a positives entry whose first line carries no ` +
|
|
85
|
+
`\`prompt:\` key: ${JSON.stringify(raw.trim())}`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (/^\s*open_files:\s*$/u.test(raw)) {
|
|
90
|
+
problems.push(
|
|
91
|
+
`${f}:${String(lineNo)}: a BLOCK-sequence \`open_files:\` — only the inline ` +
|
|
92
|
+
'`[...]` form is read, so this entry\'s files would be dropped',
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return problems;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Every positive prompt in the frozen routing-matrix corpus. Near-misses are not fires. */
|
|
101
|
+
export function readCorpusPositives(root: string): CorpusPrompt[] {
|
|
102
|
+
const dir = path.join(root, ROUTING_MATRIX_REL);
|
|
103
|
+
const out: CorpusPrompt[] = [];
|
|
104
|
+
if (!fs.existsSync(dir)) return out;
|
|
105
|
+
for (const f of fs.readdirSync(dir).filter((n) => n.endsWith('.yaml')).sort()) {
|
|
106
|
+
let inPositives = false;
|
|
107
|
+
let cur: CorpusPrompt | null = null;
|
|
108
|
+
for (const raw of fs.readFileSync(path.join(dir, f), 'utf-8').split('\n')) {
|
|
109
|
+
if (/^positives:/u.test(raw)) { inPositives = true; continue; }
|
|
110
|
+
if (/^near_misses:/u.test(raw)) { inPositives = false; continue; }
|
|
111
|
+
if (!inPositives) continue;
|
|
112
|
+
const p = /^\s*-\s*prompt:\s*"(.*)"\s*$/u.exec(raw);
|
|
113
|
+
if (p !== null) {
|
|
114
|
+
cur = { prompt: p[1] as string, openFiles: null };
|
|
115
|
+
out.push(cur);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const of = /^\s*open_files:\s*\[(.*)\]\s*$/u.exec(raw);
|
|
119
|
+
if (of !== null && cur !== null) {
|
|
120
|
+
cur.openFiles = (of[1] as string)
|
|
121
|
+
.split(',')
|
|
122
|
+
.map((x) => x.trim().replace(/^["']|["']$/gu, ''))
|
|
123
|
+
.filter((x) => x !== '');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Nearest-rank percentile over an unsorted sample. Empty sample → 0. */
|
|
131
|
+
export function percentile(xs: readonly number[], q: number): number {
|
|
132
|
+
if (xs.length === 0) return 0;
|
|
133
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
134
|
+
return s[Math.min(s.length - 1, Math.floor(q * (s.length - 1)))] as number;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface SlotFireSizes {
|
|
138
|
+
readonly slot: string;
|
|
139
|
+
readonly fires: number;
|
|
140
|
+
readonly p50: number;
|
|
141
|
+
readonly p90: number;
|
|
142
|
+
readonly max: number;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Gate-open per-fire emission sizes for the delivery concern, per slot.
|
|
147
|
+
*
|
|
148
|
+
* `pre_compact` is 0 by construction rather than by measurement: that branch of
|
|
149
|
+
* `rule_inject_hook` clears the seen-set and returns allow without writing to
|
|
150
|
+
* stdout, so there is no emission to sample.
|
|
151
|
+
*/
|
|
152
|
+
export function slotFireSizes(root: string, capBytes: number): SlotFireSizes[] {
|
|
153
|
+
const router = loadRouter(root);
|
|
154
|
+
const prompts = readCorpusPositives(root);
|
|
155
|
+
const sample = (mode: 'prompt' | 'file'): number[] => {
|
|
156
|
+
const out: number[] = [];
|
|
157
|
+
for (const r of prompts) {
|
|
158
|
+
if (mode === 'file' && (r.openFiles === null || r.openFiles.length === 0)) continue;
|
|
159
|
+
const matches =
|
|
160
|
+
mode === 'prompt'
|
|
161
|
+
? matchTierRules(router, r.prompt, null, null)
|
|
162
|
+
: matchTierRules(router, '', r.openFiles, null);
|
|
163
|
+
if (matches.length === 0) continue;
|
|
164
|
+
const sel = selectForInjection(root, matches, capBytes);
|
|
165
|
+
if (sel.selected.length > 0) out.push(sel.bytes);
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
};
|
|
169
|
+
const row = (slot: string, xs: number[]): SlotFireSizes => ({
|
|
170
|
+
slot,
|
|
171
|
+
fires: xs.length,
|
|
172
|
+
p50: percentile(xs, 0.5),
|
|
173
|
+
p90: percentile(xs, 0.9),
|
|
174
|
+
max: xs.length === 0 ? 0 : Math.max(...xs),
|
|
175
|
+
});
|
|
176
|
+
return [
|
|
177
|
+
row('user_prompt_submit', sample('prompt')),
|
|
178
|
+
row('pre_tool_use', sample('file')),
|
|
179
|
+
row('pre_compact', []),
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** The third labelled number, or the reason it could not be taken on this tree. */
|
|
184
|
+
export interface ActivationPayload {
|
|
185
|
+
/** Always the literal `activation-payload` — the label a reader and a caller key on. */
|
|
186
|
+
readonly scope: 'activation-payload';
|
|
187
|
+
/** `false` when the corpus or the router is absent; every figure is then 0. */
|
|
188
|
+
readonly measured: boolean;
|
|
189
|
+
/** Why it is unmeasured, or `null`. Never an estimate in place of a number. */
|
|
190
|
+
readonly unmeasured_reason: string | null;
|
|
191
|
+
/** The cap the sampled selection was taken at — the concern's `CAP_BYTES`. */
|
|
192
|
+
readonly cap_bytes: number;
|
|
193
|
+
readonly corpus: string;
|
|
194
|
+
readonly slots: readonly SlotFireSizes[];
|
|
195
|
+
readonly basis: string;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The runtime activation payload, or an honestly unmeasured reading.
|
|
200
|
+
*
|
|
201
|
+
* Never throws and never estimates: a missing corpus or router yields
|
|
202
|
+
* `measured: false` with the reason named, because a fabricated distribution
|
|
203
|
+
* published beside a standing saving is the exact defect this number exists to
|
|
204
|
+
* prevent.
|
|
205
|
+
*/
|
|
206
|
+
export function measureActivationPayload(root: string, capBytes: number): ActivationPayload {
|
|
207
|
+
const basis =
|
|
208
|
+
'Gate-open per-fire emission of the `rule-inject` concern over the frozen ' +
|
|
209
|
+
'`tests/eval/routing-matrix` positives, measured through the same ' +
|
|
210
|
+
'`matchTierRules` + `selectForInjection` the runtime concern calls, at the ' +
|
|
211
|
+
'concern\'s own CAP_BYTES. Bytes per fire, never tokens — the unit ' +
|
|
212
|
+
'`hook-token-budget.json` enforces.';
|
|
213
|
+
const empty = (reason: string): ActivationPayload => ({
|
|
214
|
+
scope: 'activation-payload',
|
|
215
|
+
measured: false,
|
|
216
|
+
unmeasured_reason: reason,
|
|
217
|
+
cap_bytes: capBytes,
|
|
218
|
+
corpus: ROUTING_MATRIX_REL,
|
|
219
|
+
slots: [],
|
|
220
|
+
basis,
|
|
221
|
+
});
|
|
222
|
+
if (!fs.existsSync(path.join(root, ROUTING_MATRIX_REL))) {
|
|
223
|
+
return empty(`the frozen corpus ${ROUTING_MATRIX_REL} is absent under ${root}`);
|
|
224
|
+
}
|
|
225
|
+
const shape = corpusShapeProblems(root);
|
|
226
|
+
if (shape.length > 0) {
|
|
227
|
+
return empty(
|
|
228
|
+
`${String(shape.length)} corpus entry/entries are in a shape the reader cannot parse, ` +
|
|
229
|
+
`so the distribution would be under-counted — first: ${shape[0] as string}`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
let slots: SlotFireSizes[];
|
|
233
|
+
try {
|
|
234
|
+
slots = slotFireSizes(root, capBytes);
|
|
235
|
+
} catch (err) {
|
|
236
|
+
return empty(`the fire sample could not be taken: ${(err as Error).message}`);
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
scope: 'activation-payload',
|
|
240
|
+
measured: true,
|
|
241
|
+
unmeasured_reason: null,
|
|
242
|
+
cap_bytes: capBytes,
|
|
243
|
+
corpus: ROUTING_MATRIX_REL,
|
|
244
|
+
slots,
|
|
245
|
+
basis,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
@@ -53,6 +53,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
53
53
|
|
|
54
54
|
import * as user_global_paths from './user_global_paths.js';
|
|
55
55
|
import { carveOutKeys } from '../../shared/settingsCarveOut.js';
|
|
56
|
+
import { applyRenamedKeys } from './settings_renamed_keys.js';
|
|
56
57
|
import type * as YamlModule from 'yaml';
|
|
57
58
|
|
|
58
59
|
// ESM has no `require`; `createRequire(import.meta.url)` restores it so the
|
|
@@ -956,9 +957,9 @@ export function load_agent_settings(
|
|
|
956
957
|
// precedence here is the inverse of the server family's, so the order of
|
|
957
958
|
// these three statements is the whole contract.
|
|
958
959
|
const merged: SettingsDict = template_defaults(template_path ?? undefined);
|
|
959
|
-
_deep_merge(merged, user_global_filtered);
|
|
960
|
+
_deep_merge(merged, applyRenamedKeys(user_global_filtered));
|
|
960
961
|
for (const p of cascade) {
|
|
961
|
-
const layer = _read_yaml(p) ?? {};
|
|
962
|
+
const layer = applyRenamedKeys(_read_yaml(p) ?? {});
|
|
962
963
|
if (Object.keys(layer).length > 0) {
|
|
963
964
|
_deep_merge(merged, layer);
|
|
964
965
|
}
|
|
@@ -967,6 +968,7 @@ export function load_agent_settings(
|
|
|
967
968
|
return merged;
|
|
968
969
|
}
|
|
969
970
|
|
|
971
|
+
|
|
970
972
|
/**
|
|
971
973
|
* Settings keys this package has DELETED. A leftover value from an older
|
|
972
974
|
* install is ignored — never applied by any reader — and surfaced with ONE
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The strictest path, reserved for authority —
|
|
3
|
+
* `road-to-adversarial-verification-and-long-runs` 11.1.
|
|
4
|
+
*
|
|
5
|
+
* An authority-expanding change takes four independent passes — an independent
|
|
6
|
+
* TEST author, an independent GOVERNANCE reviewer, a COUNCIL pass, and a
|
|
7
|
+
* PROVIDER-DIVERSE reviewer — plus the ratification artefact. Until the artefact
|
|
8
|
+
* carries `verdict: ratified`, the new authority is **inert**.
|
|
9
|
+
*
|
|
10
|
+
* ```
|
|
11
|
+
* NEW AUTHORITY IS INERT UNTIL RATIFIED. AN AGENT MAY AUTHOR THE CHANGE
|
|
12
|
+
* AND MAY NEVER RATIFY ITS OWN INCREASE IN POWER (ADR-268 § 4).
|
|
13
|
+
* FOUR PASSES, ALL FOUR, AND THEY MAY NOT BE THE SAME REVIEWER TWICE.
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* **Inert-until-ratified is the property that makes the rest safe.** Without it
|
|
17
|
+
* the four passes are a process an author could complete and then merge; with it
|
|
18
|
+
* the change can land, be read, and still do nothing — so the ratification is a
|
|
19
|
+
* separate act on a separate turn by a party that is not the author.
|
|
20
|
+
*
|
|
21
|
+
* Pure.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** The four passes. All four, and the list is closed. */
|
|
25
|
+
export const REQUIRED_PASSES = [
|
|
26
|
+
'independent-test-author',
|
|
27
|
+
'independent-governance-reviewer',
|
|
28
|
+
'council',
|
|
29
|
+
'provider-diverse-reviewer',
|
|
30
|
+
] as const;
|
|
31
|
+
export type Pass = (typeof REQUIRED_PASSES)[number];
|
|
32
|
+
|
|
33
|
+
export interface PassRecord {
|
|
34
|
+
readonly pass: Pass;
|
|
35
|
+
/** Who performed it. Two passes by the same identity is not two passes. */
|
|
36
|
+
readonly by: string;
|
|
37
|
+
/** The provider, so provider-diversity is checkable rather than asserted. */
|
|
38
|
+
readonly provider: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RatificationArtefact {
|
|
42
|
+
/** Anything other than `ratified` leaves the authority inert. */
|
|
43
|
+
readonly verdict: string;
|
|
44
|
+
readonly passes: readonly PassRecord[];
|
|
45
|
+
/** The author of the change under ratification. */
|
|
46
|
+
readonly author: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type AuthorityState = 'inert' | 'active';
|
|
50
|
+
|
|
51
|
+
export interface AuthorityVerdict {
|
|
52
|
+
readonly state: AuthorityState;
|
|
53
|
+
readonly blockers: readonly string[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Is the new authority active yet?
|
|
58
|
+
*
|
|
59
|
+
* Returns EVERY blocker rather than the first: the artefact is a checklist a
|
|
60
|
+
* human completes, and handing them one item at a time turns four passes into
|
|
61
|
+
* four round trips.
|
|
62
|
+
*/
|
|
63
|
+
export function authorityState(a: RatificationArtefact | null): AuthorityVerdict {
|
|
64
|
+
if (a === null) {
|
|
65
|
+
return { state: 'inert', blockers: ['no ratification artefact exists'] };
|
|
66
|
+
}
|
|
67
|
+
const blockers: string[] = [];
|
|
68
|
+
|
|
69
|
+
for (const required of REQUIRED_PASSES) {
|
|
70
|
+
if (!a.passes.some((p) => p.pass === required)) {
|
|
71
|
+
blockers.push(`missing pass: ${required}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// The author may perform NONE of the four. This is the ADR-268 § 4 clause
|
|
76
|
+
// and the one a process can satisfy on paper while violating in substance.
|
|
77
|
+
for (const p of a.passes) {
|
|
78
|
+
if (p.by === a.author) {
|
|
79
|
+
blockers.push(`${p.pass} was performed by the author (${p.by}) — an agent may not ratify its own increase in power`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Provider diversity is a property of the SET, so it is checked over the set
|
|
84
|
+
// rather than trusted from the pass that carries the name.
|
|
85
|
+
const providers = new Set(a.passes.map((p) => p.provider));
|
|
86
|
+
if (a.passes.length > 1 && providers.size < 2) {
|
|
87
|
+
blockers.push(
|
|
88
|
+
`every pass ran on one provider (${[...providers].join(', ')}) — provider-diverse means at least two`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (a.verdict !== 'ratified') {
|
|
93
|
+
blockers.push(`verdict is "${a.verdict}", not "ratified"`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { state: blockers.length === 0 ? 'active' : 'inert', blockers };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** `true` only for an artefact that clears everything. */
|
|
100
|
+
export function isRatified(a: RatificationArtefact | null): boolean {
|
|
101
|
+
return authorityState(a).state === 'active';
|
|
102
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read a prefix-stable surface set out of a git ref, into a scratch tree.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS
|
|
5
|
+
* ---------------
|
|
6
|
+
* `road-to-delivery-for-every-host` 4.4 replaces a STORED standing-payload
|
|
7
|
+
* ceiling with a MEASURED one: `max(design_ceiling, payload_at_base_ref)`. An
|
|
8
|
+
* AI council (2/2, 2026-09-10) converged on that formula and on the reason — a
|
|
9
|
+
* stored ceiling stays where it was put until a human lowers it, so payload a
|
|
10
|
+
* merge removed can be added straight back into the space it freed. A measured
|
|
11
|
+
* ceiling captures every merged reduction automatically.
|
|
12
|
+
*
|
|
13
|
+
* To measure the base ref, the base tree has to exist somewhere. This module is
|
|
14
|
+
* that step and nothing else: it materialises the declared prefix-stable roots
|
|
15
|
+
* at a ref and hands back a directory. What to measure, and what to do with the
|
|
16
|
+
* number, belong to the caller.
|
|
17
|
+
*
|
|
18
|
+
* WHY NOT `git archive`, WHICH IS THE OBVIOUS TOOL
|
|
19
|
+
* ------------------------------------------------
|
|
20
|
+
* Because it silently drops declared surfaces. `git archive` honours
|
|
21
|
+
* `export-ignore`, and `.gitattributes` in this repository marks `/CLAUDE.md`
|
|
22
|
+
* export-ignore so release tarballs stay clean. `CLAUDE.md` is ALSO a declared
|
|
23
|
+
* prefix-stable surface worth 746 tok. Measured while building this module:
|
|
24
|
+
* `git archive HEAD -- CLAUDE.md | tar -t` emits **zero entries and exits 0**,
|
|
25
|
+
* so an archive-based base reading came back at 137,667 against a direct
|
|
26
|
+
* reading of 138,413 — 746 tok light, on a tree nobody had changed. A light
|
|
27
|
+
* base reading is a TIGHT ceiling, so every pull request would have reddened by
|
|
28
|
+
* 746 tokens it did not add.
|
|
29
|
+
*
|
|
30
|
+
* That is a packaging concern leaking into a measurement, and the two must not
|
|
31
|
+
* share a switch. `git ls-tree` + `git cat-file` read the object graph directly
|
|
32
|
+
* and have no notion of `export-ignore`, so the surface set measured here is
|
|
33
|
+
* the surface set the registry declares — which is the only definition this
|
|
34
|
+
* gate may use.
|
|
35
|
+
*
|
|
36
|
+
* SYMLINKED SURFACES ARE CLOSED OVER
|
|
37
|
+
* -----------------------------------
|
|
38
|
+
* `CLAUDE.md` is a git symlink (mode 120000) to `AGENTS.md`, which is not
|
|
39
|
+
* itself a declared surface. Materialising only the declared roots would leave
|
|
40
|
+
* a dangling link, which the census reads as an absent file — the same silent
|
|
41
|
+
* 746-token shortfall by a second route. Link targets are resolved at the git
|
|
42
|
+
* layer (a 120000 blob's content IS its target) and materialised alongside, to
|
|
43
|
+
* a fixpoint.
|
|
44
|
+
*
|
|
45
|
+
* FAILURE IS A VALUE, NEVER AN EXCEPTION AND NEVER A ZERO
|
|
46
|
+
* -------------------------------------------------------
|
|
47
|
+
* Every failure path returns `tree: null` with a stated reason. A census over a
|
|
48
|
+
* tree that could not be built returns a small number rather than an error, and
|
|
49
|
+
* a small base reading is a tight ceiling — the direction that reds honest
|
|
50
|
+
* work. The caller decides whether an unreadable base skips or refuses; this
|
|
51
|
+
* module refuses to guess on its behalf.
|
|
52
|
+
*/
|
|
53
|
+
import { spawnSync } from 'node:child_process';
|
|
54
|
+
import * as fs from 'node:fs';
|
|
55
|
+
import * as os from 'node:os';
|
|
56
|
+
import * as path from 'node:path';
|
|
57
|
+
|
|
58
|
+
import { prefixStableRoots } from './prefix_stable_surfaces.js';
|
|
59
|
+
|
|
60
|
+
/** Injection seam: run git, capture stdout as a buffer plus the exit status. */
|
|
61
|
+
export type GitBufferRunner = (
|
|
62
|
+
args: readonly string[],
|
|
63
|
+
cwd: string,
|
|
64
|
+
stdin?: string,
|
|
65
|
+
) => { ok: boolean; stdout: Buffer };
|
|
66
|
+
|
|
67
|
+
export const realGitBuffer: GitBufferRunner = (args, cwd, stdin) => {
|
|
68
|
+
const r = spawnSync('git', [...args], {
|
|
69
|
+
cwd,
|
|
70
|
+
maxBuffer: 512 * 1024 * 1024,
|
|
71
|
+
...(stdin === undefined ? {} : { input: stdin }),
|
|
72
|
+
});
|
|
73
|
+
return { ok: r.status === 0, stdout: r.stdout ?? Buffer.alloc(0) };
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
interface TreeEntry {
|
|
77
|
+
mode: string;
|
|
78
|
+
sha: string;
|
|
79
|
+
/** Repo-relative path. */
|
|
80
|
+
rel: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface TreeAtRef {
|
|
84
|
+
/** Absolute path of the scratch tree, or `null` when it could not be built. */
|
|
85
|
+
tree: string | null;
|
|
86
|
+
/** Why it could not be built. `null` on success. */
|
|
87
|
+
note: string | null;
|
|
88
|
+
/** Repo-relative paths materialised. Empty on failure. */
|
|
89
|
+
files: string[];
|
|
90
|
+
/** Call when done. Safe to call on a failed reading. */
|
|
91
|
+
dispose: () => void;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** How deep a chain of symlinked surfaces is followed before it is left alone. */
|
|
95
|
+
const MAX_LINK_DEPTH = 4;
|
|
96
|
+
|
|
97
|
+
function normalise(p: string): string {
|
|
98
|
+
return p.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Every blob under the given paths at `ref`.
|
|
103
|
+
*
|
|
104
|
+
* `-z` because a path may contain a character `git` would otherwise quote, and
|
|
105
|
+
* a quoted path silently names a file that does not exist.
|
|
106
|
+
*/
|
|
107
|
+
function listTree(git: GitBufferRunner, repoRoot: string, ref: string, paths: readonly string[]): TreeEntry[] | null {
|
|
108
|
+
const r = git(['ls-tree', '-r', '-z', '--full-tree', ref, '--', ...paths], repoRoot);
|
|
109
|
+
if (!r.ok) return null;
|
|
110
|
+
const out: TreeEntry[] = [];
|
|
111
|
+
for (const record of r.stdout.toString('utf-8').split('\0')) {
|
|
112
|
+
if (record === '') continue;
|
|
113
|
+
// `<mode> <type> <sha>\t<path>`
|
|
114
|
+
const tab = record.indexOf('\t');
|
|
115
|
+
if (tab === -1) continue;
|
|
116
|
+
const meta = record.slice(0, tab).split(' ');
|
|
117
|
+
const mode = meta[0];
|
|
118
|
+
const sha = meta[2];
|
|
119
|
+
if (mode === undefined || sha === undefined) continue;
|
|
120
|
+
out.push({ mode, sha, rel: normalise(record.slice(tab + 1)) });
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Blob contents for a list of entries, in one `git cat-file --batch` call.
|
|
127
|
+
*
|
|
128
|
+
* One request line per ENTRY rather than per distinct sha: two files with
|
|
129
|
+
* identical content share a sha, and de-duplicating would misalign responses
|
|
130
|
+
* against entries for no measurable gain at this size (728 blobs).
|
|
131
|
+
*/
|
|
132
|
+
function readBlobs(git: GitBufferRunner, repoRoot: string, entries: readonly TreeEntry[]): Buffer[] | null {
|
|
133
|
+
if (entries.length === 0) return [];
|
|
134
|
+
const r = git(['cat-file', '--batch'], repoRoot, entries.map((e) => e.sha).join('\n') + '\n');
|
|
135
|
+
if (!r.ok) return null;
|
|
136
|
+
const buf = r.stdout;
|
|
137
|
+
const out: Buffer[] = [];
|
|
138
|
+
let at = 0;
|
|
139
|
+
for (let i = 0; i < entries.length; i += 1) {
|
|
140
|
+
const nl = buf.indexOf(0x0a, at);
|
|
141
|
+
if (nl === -1) return null;
|
|
142
|
+
// `<sha> <type> <size>` — a missing object answers `<name> missing`.
|
|
143
|
+
const header = buf.subarray(at, nl).toString('utf-8').split(' ');
|
|
144
|
+
const size = Number(header[2]);
|
|
145
|
+
if (!Number.isFinite(size)) return null;
|
|
146
|
+
const start = nl + 1;
|
|
147
|
+
out.push(buf.subarray(start, start + size));
|
|
148
|
+
// Each payload is followed by a single trailing newline.
|
|
149
|
+
at = start + size + 1;
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Materialise the declared prefix-stable roots at `ref` into a scratch tree.
|
|
156
|
+
*
|
|
157
|
+
* `roots` defaults to the canonical registry rather than a literal list here:
|
|
158
|
+
* `prefix_stable_surfaces.ts` is the one place that boundary is declared, and a
|
|
159
|
+
* second copy is the drift shape this repository has already paid for.
|
|
160
|
+
*/
|
|
161
|
+
export function extractSurfacesAtRef(opts: {
|
|
162
|
+
repoRoot: string;
|
|
163
|
+
ref: string;
|
|
164
|
+
roots?: readonly string[];
|
|
165
|
+
git?: GitBufferRunner;
|
|
166
|
+
}): TreeAtRef {
|
|
167
|
+
const git = opts.git ?? realGitBuffer;
|
|
168
|
+
const declared = (opts.roots ?? prefixStableRoots()).map(normalise).filter((r) => r !== '');
|
|
169
|
+
const noop = (): void => undefined;
|
|
170
|
+
if (declared.length === 0) {
|
|
171
|
+
return {
|
|
172
|
+
tree: null,
|
|
173
|
+
files: [],
|
|
174
|
+
note: 'no prefix-stable roots are declared, so there is nothing to read at the base ref',
|
|
175
|
+
dispose: noop,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const entries = listTree(git, opts.repoRoot, ref(opts.ref), declared);
|
|
180
|
+
if (entries === null) {
|
|
181
|
+
return {
|
|
182
|
+
tree: null,
|
|
183
|
+
files: [],
|
|
184
|
+
note: `'${opts.ref}' could not be read as a tree here — the ref is unreachable in this checkout`,
|
|
185
|
+
dispose: noop,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (entries.length === 0) {
|
|
189
|
+
return {
|
|
190
|
+
tree: null,
|
|
191
|
+
files: [],
|
|
192
|
+
note:
|
|
193
|
+
`no declared prefix-stable root exists at ${opts.ref} — ` +
|
|
194
|
+
'the surfaces moved, or the ref predates them',
|
|
195
|
+
dispose: noop,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Close over symlinked surfaces: a dangling link measures as an absent file.
|
|
200
|
+
const seen = new Set(entries.map((e) => e.rel));
|
|
201
|
+
let frontier = entries.filter((e) => e.mode === '120000');
|
|
202
|
+
for (let depth = 0; depth < MAX_LINK_DEPTH && frontier.length > 0; depth += 1) {
|
|
203
|
+
const blobs = readBlobs(git, opts.repoRoot, frontier);
|
|
204
|
+
if (blobs === null) break;
|
|
205
|
+
const wanted: string[] = [];
|
|
206
|
+
frontier.forEach((e, i) => {
|
|
207
|
+
const target = (blobs[i] ?? Buffer.alloc(0)).toString('utf-8').trim();
|
|
208
|
+
if (target === '' || path.isAbsolute(target)) return;
|
|
209
|
+
const resolved = normalise(path.posix.join(path.posix.dirname(e.rel), target));
|
|
210
|
+
if (resolved.startsWith('..') || seen.has(resolved)) return;
|
|
211
|
+
wanted.push(resolved);
|
|
212
|
+
});
|
|
213
|
+
if (wanted.length === 0) break;
|
|
214
|
+
const extra = listTree(git, opts.repoRoot, ref(opts.ref), wanted);
|
|
215
|
+
if (extra === null || extra.length === 0) break;
|
|
216
|
+
const fresh = extra.filter((e) => !seen.has(e.rel));
|
|
217
|
+
for (const e of fresh) {
|
|
218
|
+
seen.add(e.rel);
|
|
219
|
+
entries.push(e);
|
|
220
|
+
}
|
|
221
|
+
frontier = fresh.filter((e) => e.mode === '120000');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const blobs = readBlobs(git, opts.repoRoot, entries);
|
|
225
|
+
if (blobs === null) {
|
|
226
|
+
return {
|
|
227
|
+
tree: null,
|
|
228
|
+
files: [],
|
|
229
|
+
note: `the blobs at ${opts.ref} could not be read back — git cat-file returned an unusable stream`,
|
|
230
|
+
dispose: noop,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let tmp: string;
|
|
235
|
+
try {
|
|
236
|
+
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'payload-at-ref-'));
|
|
237
|
+
} catch (err) {
|
|
238
|
+
return {
|
|
239
|
+
tree: null,
|
|
240
|
+
files: [],
|
|
241
|
+
note: `a scratch directory could not be created: ${(err as Error).message}`,
|
|
242
|
+
dispose: noop,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
const dispose = (): void => {
|
|
246
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
entries.forEach((e, i) => {
|
|
251
|
+
const dest = path.join(tmp, e.rel);
|
|
252
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
253
|
+
const body = blobs[i] ?? Buffer.alloc(0);
|
|
254
|
+
if (e.mode === '120000') {
|
|
255
|
+
// Written as a real symlink so the census resolves it exactly as
|
|
256
|
+
// it does in the working tree — the target is materialised too.
|
|
257
|
+
fs.symlinkSync(body.toString('utf-8').trim(), dest);
|
|
258
|
+
} else {
|
|
259
|
+
fs.writeFileSync(dest, body);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
} catch (err) {
|
|
263
|
+
dispose();
|
|
264
|
+
return {
|
|
265
|
+
tree: null,
|
|
266
|
+
files: [],
|
|
267
|
+
note: `writing the scratch tree for ${opts.ref} failed: ${(err as Error).message}`,
|
|
268
|
+
dispose: noop,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return { tree: tmp, files: entries.map((e) => e.rel), note: null, dispose };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** `git ls-tree` needs a tree-ish; `<ref>` alone is one, but guard the empty case. */
|
|
276
|
+
function ref(r: string): string {
|
|
277
|
+
return r.trim();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export interface PayloadAtRef {
|
|
281
|
+
ref: string;
|
|
282
|
+
/** Total tokens at the ref, or `null` when the tree could not be read. */
|
|
283
|
+
tokens: number | null;
|
|
284
|
+
note: string | null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Measure the payload at `ref` with the caller's own census.
|
|
289
|
+
*
|
|
290
|
+
* The census is injected rather than imported so this module carries none of
|
|
291
|
+
* the gate's measurement code, and so a test can prove the materialisation
|
|
292
|
+
* independently of what is counted. The CALLER passing its own census is also
|
|
293
|
+
* what makes base and head the same measurement — the property that keeps a
|
|
294
|
+
* measured ceiling from drifting against the number it bounds.
|
|
295
|
+
*/
|
|
296
|
+
export function measurePayloadAtRef(opts: {
|
|
297
|
+
repoRoot: string;
|
|
298
|
+
ref: string;
|
|
299
|
+
measure: (treeRoot: string) => number;
|
|
300
|
+
git?: GitBufferRunner;
|
|
301
|
+
}): PayloadAtRef {
|
|
302
|
+
if (opts.ref.trim() === '') {
|
|
303
|
+
return { ref: opts.ref, tokens: null, note: 'no base ref was resolved, so there is no tree to measure' };
|
|
304
|
+
}
|
|
305
|
+
const gitOpt = opts.git === undefined ? {} : { git: opts.git };
|
|
306
|
+
const t = extractSurfacesAtRef({ repoRoot: opts.repoRoot, ref: opts.ref, ...gitOpt });
|
|
307
|
+
if (t.tree === null) return { ref: opts.ref, tokens: null, note: t.note };
|
|
308
|
+
try {
|
|
309
|
+
return { ref: opts.ref, tokens: opts.measure(t.tree), note: null };
|
|
310
|
+
} catch (err) {
|
|
311
|
+
return { ref: opts.ref, tokens: null, note: `the census failed on the base tree: ${(err as Error).message}` };
|
|
312
|
+
} finally {
|
|
313
|
+
t.dispose();
|
|
314
|
+
}
|
|
315
|
+
}
|