@event4u/agent-config 16.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 +297 -137
- 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 +74 -27
- 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 +47 -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/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/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-H32DGUJH.js → chunk-P6ZHP5W2.js} +1 -1
- 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-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 +90 -56
- package/dist/cli-delegate/cmd_doctor.js +7 -5
- package/dist/cli-delegate/cmd_explain.js +2 -2
- 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 +4 -4
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +2 -2
- package/dist/cli-delegate/cmd_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 +2 -2
- 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 +1 -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 +85 -38
- 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 +1832 -1007
- 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 +2007 -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 +398 -39
- 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_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/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 +21 -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/loop-surfaces.yaml +215 -0
- package/src/config/platform-anchor.json +3 -2
- 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/_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/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/forge_protection.ts +267 -0
- package/src/scripts/_lib/hook_platform_keys.ts +27 -0
- 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/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/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/rides_along.ts +102 -0
- package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
- package/src/scripts/_lib/test_delta.ts +166 -0
- package/src/scripts/_lib/test_provenance.ts +171 -0
- 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_enforcement_matrix.ts +624 -0
- package/src/scripts/check_gate_reachability.ts +173 -10
- package/src/scripts/check_generator_sync.ts +954 -0
- package/src/scripts/check_preamble_payload_budget.ts +75 -2
- package/src/scripts/check_release_highlights.ts +33 -1
- package/src/scripts/check_release_holds.ts +567 -0
- 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 +88 -9
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/design_pass_hook.ts +128 -3
- 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/rule_inject_hook.ts +49 -1
- package/src/scripts/hooks/run_continuation_hook.ts +17 -20
- package/src/scripts/hooks_doctor.ts +85 -0
- package/src/scripts/hooks_status.ts +19 -0
- package/src/scripts/install.ts +106 -206
- package/src/scripts/janitor.ts +7 -0
- 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/new_roadmap.ts +37 -0
- package/src/scripts/orchestration_record.ts +0 -5
- package/src/scripts/release.ts +14 -98
- package/src/scripts/release_drill.ts +267 -1
- package/src/scripts/release_publication.ts +51 -0
- package/src/scripts/report_enforcement_drift.ts +566 -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/trigger_eval_grandfather.json +0 -1
- package/src/scripts/ui_conformance_probe.ts +622 -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,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Council transport and cost — `road-to-adversarial-verification-and-long-runs`
|
|
3
|
+
* 10.1.
|
|
4
|
+
*
|
|
5
|
+
* The posture is CLI → CLI quota exhausted → API within the ceiling → API over
|
|
6
|
+
* the ceiling → **pause and report**. The last rung is the step's whole point:
|
|
7
|
+
* over the ceiling the run does not ask whether to buy more technical API usage.
|
|
8
|
+
* It reports.
|
|
9
|
+
*
|
|
10
|
+
* ```
|
|
11
|
+
* AN OVER-CEILING COUNCIL REQUIREMENT PRODUCES A REPORT, NEVER A QUESTION.
|
|
12
|
+
* TECHNICAL API SPEND IS NOT A DECISION THE OWNER IS ASKED FOR MID-RUN.
|
|
13
|
+
* BUSINESS SPEND IS A TYPED OP AND A DIFFERENT CATEGORY ENTIRELY.
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* **Why a report rather than an ask, when both interrupt.** They do not
|
|
17
|
+
* interrupt the same way. An ask BLOCKS: the run stops until an answer arrives,
|
|
18
|
+
* and the thing being asked about — a few dollars of inference — is worth less
|
|
19
|
+
* than the run's remaining work. A report does not block: it says what needed the
|
|
20
|
+
* council, why the cheaper route was unavailable, what it would cost, and — the
|
|
21
|
+
* load-bearing field — **what can still proceed without it**. Most of a mission
|
|
22
|
+
* can.
|
|
23
|
+
*
|
|
24
|
+
* Pure.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** The routes, cheapest first. */
|
|
28
|
+
export type Route = 'cli' | 'api' | 'paused';
|
|
29
|
+
|
|
30
|
+
export interface TransportInput {
|
|
31
|
+
/** Is the CLI route configured and within quota? */
|
|
32
|
+
readonly cliAvailable: boolean;
|
|
33
|
+
/** Why the CLI is unavailable, when it is not. */
|
|
34
|
+
readonly cliUnavailableReason: string | null;
|
|
35
|
+
/** Estimated USD for the API route. */
|
|
36
|
+
readonly estimateUsd: number;
|
|
37
|
+
/** The run's remaining technical-spend ceiling in USD. */
|
|
38
|
+
readonly ceilingUsd: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface PauseReport {
|
|
42
|
+
readonly needed: string;
|
|
43
|
+
readonly cliUnavailableBecause: string;
|
|
44
|
+
readonly estimateUsd: number;
|
|
45
|
+
readonly ceilingUsd: number;
|
|
46
|
+
readonly missionState: string;
|
|
47
|
+
/** What the run continues with meanwhile. Empty is a real, reportable answer. */
|
|
48
|
+
readonly canStillProceed: readonly string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface TransportDecision {
|
|
52
|
+
readonly route: Route;
|
|
53
|
+
readonly reason: string;
|
|
54
|
+
/** Present only on `paused`. */
|
|
55
|
+
readonly report: PauseReport | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface PauseContext {
|
|
59
|
+
readonly needed: string;
|
|
60
|
+
readonly missionState: string;
|
|
61
|
+
readonly canStillProceed: readonly string[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Route one council requirement.
|
|
66
|
+
*
|
|
67
|
+
* A ceiling of zero pauses rather than dividing by it, and an estimate exactly
|
|
68
|
+
* AT the ceiling is within it — the ceiling is a limit, not an exclusive bound,
|
|
69
|
+
* and reading it the other way would pause a run that had budgeted exactly.
|
|
70
|
+
*/
|
|
71
|
+
export function routeCouncil(input: TransportInput, ctx: PauseContext): TransportDecision {
|
|
72
|
+
if (input.cliAvailable) {
|
|
73
|
+
return { route: 'cli', reason: 'the CLI route is available and within quota', report: null };
|
|
74
|
+
}
|
|
75
|
+
const why = input.cliUnavailableReason ?? 'unstated';
|
|
76
|
+
if (input.estimateUsd <= input.ceilingUsd) {
|
|
77
|
+
return {
|
|
78
|
+
route: 'api',
|
|
79
|
+
reason: `CLI unavailable (${why}); the API estimate $${input.estimateUsd.toFixed(2)} is within the $${input.ceilingUsd.toFixed(2)} ceiling`,
|
|
80
|
+
report: null,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
route: 'paused',
|
|
85
|
+
reason: `the API estimate $${input.estimateUsd.toFixed(2)} exceeds the $${input.ceilingUsd.toFixed(2)} ceiling`,
|
|
86
|
+
report: {
|
|
87
|
+
needed: ctx.needed,
|
|
88
|
+
cliUnavailableBecause: why,
|
|
89
|
+
estimateUsd: input.estimateUsd,
|
|
90
|
+
ceilingUsd: input.ceilingUsd,
|
|
91
|
+
missionState: ctx.missionState,
|
|
92
|
+
canStillProceed: ctx.canStillProceed,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The six fields a pause report must carry. */
|
|
98
|
+
export const REPORT_FIELDS: readonly (keyof PauseReport)[] = [
|
|
99
|
+
'needed',
|
|
100
|
+
'cliUnavailableBecause',
|
|
101
|
+
'estimateUsd',
|
|
102
|
+
'ceilingUsd',
|
|
103
|
+
'missionState',
|
|
104
|
+
'canStillProceed',
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Render the report.
|
|
109
|
+
*
|
|
110
|
+
* It ends on what can still proceed, and that placement is deliberate: a reader
|
|
111
|
+
* who stops after the cost has read a complaint, and a reader who reaches the
|
|
112
|
+
* last line has read a status. Nothing here is phrased as a question, and the
|
|
113
|
+
* renderer emits no `?` — asserted by a test, because the failure mode is a
|
|
114
|
+
* report that drifts into an ask one helpful sentence at a time.
|
|
115
|
+
*/
|
|
116
|
+
export function renderReport(r: PauseReport): string {
|
|
117
|
+
const proceeding =
|
|
118
|
+
r.canStillProceed.length > 0
|
|
119
|
+
? r.canStillProceed.map((x) => ` - ${x}`).join('\n')
|
|
120
|
+
: ' - nothing; the mission is blocked on this council pass alone';
|
|
121
|
+
return [
|
|
122
|
+
`Council paused: ${r.needed}`,
|
|
123
|
+
`CLI route unavailable: ${r.cliUnavailableBecause}`,
|
|
124
|
+
`API estimate $${r.estimateUsd.toFixed(2)} over the $${r.ceilingUsd.toFixed(2)} technical ceiling`,
|
|
125
|
+
`Mission state: ${r.missionState}`,
|
|
126
|
+
'Proceeding meanwhile with:',
|
|
127
|
+
proceeding,
|
|
128
|
+
].join('\n');
|
|
129
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is a PR delivery-ready? — `road-to-adversarial-verification-and-long-runs` 3.1.
|
|
3
|
+
*
|
|
4
|
+
* The step's acceptance clause is that *a fixture PR with a disabled required
|
|
5
|
+
* check cannot reach delivery-ready*, and that is the one case a naive reader
|
|
6
|
+
* gets wrong: a PR whose required context was SKIPPED has a green rollup. GitHub
|
|
7
|
+
* reports `SKIPPED` as a non-failure, so a predicate that asks "did anything
|
|
8
|
+
* fail" answers no, and the gate the context was supposed to enforce did not run.
|
|
9
|
+
*
|
|
10
|
+
* So this asks two questions rather than one: did every required context report,
|
|
11
|
+
* and did each of those reports actually pass. A context that is missing from
|
|
12
|
+
* the rollup entirely fails the first; a context present but skipped, cancelled
|
|
13
|
+
* or neutral fails the second.
|
|
14
|
+
*
|
|
15
|
+
* Pure — the caller supplies the rollup and the required set, so both polarities
|
|
16
|
+
* are testable without a forge.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** One check as a forge rollup reports it. */
|
|
20
|
+
export interface CheckRun {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
/** `SUCCESS` · `FAILURE` · `SKIPPED` · `CANCELLED` · `NEUTRAL` · `null` while running. */
|
|
23
|
+
readonly conclusion: string | null;
|
|
24
|
+
/** `COMPLETED` · `IN_PROGRESS` · `QUEUED`. */
|
|
25
|
+
readonly status: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Why a PR is not delivery-ready. Empty means it is. */
|
|
29
|
+
export type DeliveryBlock =
|
|
30
|
+
| { readonly kind: 'required-context-absent'; readonly context: string }
|
|
31
|
+
| { readonly kind: 'required-context-not-passing'; readonly context: string; readonly conclusion: string }
|
|
32
|
+
| { readonly kind: 'still-running'; readonly context: string }
|
|
33
|
+
| { readonly kind: 'stale-head'; readonly observed: string; readonly head: string };
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The only conclusion that counts as a required check having done its job.
|
|
37
|
+
*
|
|
38
|
+
* `SKIPPED` is deliberately NOT here, and it is the reason this module exists:
|
|
39
|
+
* a skipped required check is an unenforced gate wearing a green rollup.
|
|
40
|
+
* `NEUTRAL` and `CANCELLED` are excluded on the same ground — a check that
|
|
41
|
+
* reached no verdict enforced nothing, whatever colour it rendered.
|
|
42
|
+
*/
|
|
43
|
+
const PASSING = 'SUCCESS';
|
|
44
|
+
|
|
45
|
+
export interface DeliveryInput {
|
|
46
|
+
/** Contexts the forge requires, from `forge_protection`'s ruleset read. */
|
|
47
|
+
readonly requiredContexts: readonly string[];
|
|
48
|
+
/** The rollup on the head under test. */
|
|
49
|
+
readonly checks: readonly CheckRun[];
|
|
50
|
+
/** The head CI actually observed. */
|
|
51
|
+
readonly observedHead: string;
|
|
52
|
+
/** The branch head right now. */
|
|
53
|
+
readonly head: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Every reason this PR is not delivery-ready, in report order.
|
|
58
|
+
*
|
|
59
|
+
* Returns ALL blocks rather than the first: a run that fixes one and re-pushes
|
|
60
|
+
* to discover the next has spent a CI cycle per block, which is the cost layer 1
|
|
61
|
+
* of the delivery contract exists to avoid paying at layer 4.
|
|
62
|
+
*/
|
|
63
|
+
export function deliveryBlocks(input: DeliveryInput): DeliveryBlock[] {
|
|
64
|
+
const out: DeliveryBlock[] = [];
|
|
65
|
+
const byName = new Map(input.checks.map((c) => [c.name, c]));
|
|
66
|
+
|
|
67
|
+
for (const context of input.requiredContexts) {
|
|
68
|
+
const run = byName.get(context);
|
|
69
|
+
if (run === undefined) {
|
|
70
|
+
out.push({ kind: 'required-context-absent', context });
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (run.status !== 'COMPLETED') {
|
|
74
|
+
out.push({ kind: 'still-running', context });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (run.conclusion !== PASSING) {
|
|
78
|
+
out.push({
|
|
79
|
+
kind: 'required-context-not-passing',
|
|
80
|
+
context,
|
|
81
|
+
conclusion: run.conclusion ?? 'null',
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Checked LAST and always: a green rollup on a head that is no longer the
|
|
87
|
+
// branch head is a verdict about a different tree. `ci_settle` names the head
|
|
88
|
+
// its verdict is about precisely so this comparison is possible.
|
|
89
|
+
if (input.observedHead !== input.head) {
|
|
90
|
+
out.push({ kind: 'stale-head', observed: input.observedHead, head: input.head });
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** True only when nothing blocks. */
|
|
96
|
+
export function isDeliveryReady(input: DeliveryInput): boolean {
|
|
97
|
+
return deliveryBlocks(input).length === 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** One human-readable line per block. */
|
|
101
|
+
export function renderBlock(b: DeliveryBlock): string {
|
|
102
|
+
switch (b.kind) {
|
|
103
|
+
case 'required-context-absent':
|
|
104
|
+
return `required check "${b.context}" did not report at all`;
|
|
105
|
+
case 'required-context-not-passing':
|
|
106
|
+
return `required check "${b.context}" concluded ${b.conclusion}, not SUCCESS — a skipped or cancelled required check is an unenforced gate, not a pass`;
|
|
107
|
+
case 'still-running':
|
|
108
|
+
return `required check "${b.context}" has not completed`;
|
|
109
|
+
case 'stale-head':
|
|
110
|
+
return `CI was observed on ${b.observed} but the branch head is ${b.head} — the verdict describes a different tree`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The five `forge_protection` rows, read from the forge rather than guessed.
|
|
3
|
+
*
|
|
4
|
+
* `road-to-adversarial-verification-and-long-runs` Phase 3.2. ADR-268 § 3 trades
|
|
5
|
+
* the owner's confirmation for a mechanical check, and branch protection is the
|
|
6
|
+
* check it trades for — so a run that cannot say whether protection exists has
|
|
7
|
+
* not established the thing the trade depends on.
|
|
8
|
+
*
|
|
9
|
+
* **Every row carries the API call its value came from, and a row that was not
|
|
10
|
+
* read says so.** That is the whole shape: the step's word is *read, never
|
|
11
|
+
* guessed*, and the only way a reader can tell one from the other is if the
|
|
12
|
+
* provenance travels with the value. A row is `satisfied` / `unsatisfied` only
|
|
13
|
+
* when a named call produced it; otherwise it is `unread`, which is a third
|
|
14
|
+
* state and never a false.
|
|
15
|
+
*
|
|
16
|
+
* **Rulesets, not the classic branch-protection endpoint.** On this repository
|
|
17
|
+
* `repos/<owner>/<repo>/branches/main/protection` returns `404 {"message":"Branch
|
|
18
|
+
* not protected"}` while an active ruleset protects the default branch — so a
|
|
19
|
+
* checker reading the classic endpoint reports "unprotected" for a repository
|
|
20
|
+
* that is protected. The `forge-protection-settings` blocker was re-scoped for
|
|
21
|
+
* exactly this reason on 2026-09-10, and the re-scope forbids treating that 404
|
|
22
|
+
* as absence of protection. No ruleset ID is pinned: rulesets can be replaced or
|
|
23
|
+
* split, and pinning one would break on the first change.
|
|
24
|
+
*
|
|
25
|
+
* Pure. The network lives in the caller, so both polarities are testable offline.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import type { BypassActor, RulesetDetail } from './platform_anchor.js';
|
|
29
|
+
|
|
30
|
+
/** Whether a row was established, refuted, or never read. */
|
|
31
|
+
export type RowState = 'satisfied' | 'unsatisfied' | 'unread';
|
|
32
|
+
|
|
33
|
+
/** One `forge_protection` row. */
|
|
34
|
+
export interface ProtectionRow {
|
|
35
|
+
/** Stable key. Rows are addressed by this, never by position. */
|
|
36
|
+
readonly id: string;
|
|
37
|
+
readonly state: RowState;
|
|
38
|
+
/**
|
|
39
|
+
* The forge API call this value came from — or would come from, when the row
|
|
40
|
+
* is `unread`. Never empty: a value whose provenance is unstated is the
|
|
41
|
+
* guess this block exists to refuse.
|
|
42
|
+
*/
|
|
43
|
+
readonly source: string;
|
|
44
|
+
/** One line a human can act on when the row is not satisfied. */
|
|
45
|
+
readonly detail: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The five rows, in the order the blocker's own table lists them. */
|
|
49
|
+
export const PROTECTION_ROW_IDS: readonly string[] = [
|
|
50
|
+
'default_branch_protected',
|
|
51
|
+
'required_checks_present',
|
|
52
|
+
'force_push_disabled',
|
|
53
|
+
'auto_merge_available',
|
|
54
|
+
'deploy_via_pipeline_only',
|
|
55
|
+
] as const;
|
|
56
|
+
|
|
57
|
+
/** What a caller must supply for a row to be readable. `null` = not read. */
|
|
58
|
+
export interface ForgeReading {
|
|
59
|
+
/** `repos/<repo>/rulesets` + `/rulesets/<id>`, or `null` when unread. */
|
|
60
|
+
readonly rulesets: readonly RulesetDetail[] | null;
|
|
61
|
+
/** The repository's default branch, or `null` when unread. */
|
|
62
|
+
readonly defaultBranch: string | null;
|
|
63
|
+
/** `repos/<repo>` → `allow_auto_merge`, or `null` when unread. */
|
|
64
|
+
readonly allowAutoMerge: boolean | null;
|
|
65
|
+
/**
|
|
66
|
+
* `repos/<repo>/environments` → whether every environment with deployment
|
|
67
|
+
* branch policies restricts them, or `null` when unread.
|
|
68
|
+
*
|
|
69
|
+
* Deliberately a boolean the caller derives rather than a raw payload: what
|
|
70
|
+
* the row asserts is a property of the SET of environments, and handing this
|
|
71
|
+
* module the raw list would put that judgement in two places.
|
|
72
|
+
*/
|
|
73
|
+
readonly deployRestricted: boolean | null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const SOURCE = {
|
|
77
|
+
rulesets: 'GET repos/{owner}/{repo}/rulesets + GET .../rulesets/{id}',
|
|
78
|
+
repo: 'GET repos/{owner}/{repo} (allow_auto_merge)',
|
|
79
|
+
environments: 'GET repos/{owner}/{repo}/environments (deployment_branch_policy)',
|
|
80
|
+
} as const;
|
|
81
|
+
|
|
82
|
+
/** `true` when this ruleset is active and targets branches. */
|
|
83
|
+
function isActiveBranchRuleset(rs: RulesetDetail): boolean {
|
|
84
|
+
return rs.enforcement === 'active' && rs.target === 'branch';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** `true` when the ruleset's ref conditions reach the default branch. */
|
|
88
|
+
function reachesDefaultBranch(rs: RulesetDetail, defaultBranch: string): boolean {
|
|
89
|
+
const include = rs.conditions?.ref_name?.include ?? [];
|
|
90
|
+
return include.some(
|
|
91
|
+
(p) => p === '~DEFAULT_BRANCH' || p === '~ALL' || p === `refs/heads/${defaultBranch}`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function hasRule(rs: RulesetDetail, type: string): boolean {
|
|
96
|
+
return (rs.rules ?? []).some((r) => r.type === type);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function requiredContexts(rs: RulesetDetail): string[] {
|
|
100
|
+
for (const r of rs.rules ?? []) {
|
|
101
|
+
if (r.type !== 'required_status_checks') continue;
|
|
102
|
+
const p = r.parameters;
|
|
103
|
+
if (p === null || typeof p !== 'object') continue;
|
|
104
|
+
const list = (p as Record<string, unknown>)['required_status_checks'];
|
|
105
|
+
if (!Array.isArray(list)) continue;
|
|
106
|
+
return list
|
|
107
|
+
.map((e) =>
|
|
108
|
+
e !== null && typeof e === 'object'
|
|
109
|
+
? (e as Record<string, unknown>)['context']
|
|
110
|
+
: null,
|
|
111
|
+
)
|
|
112
|
+
.filter((c): c is string => typeof c === 'string');
|
|
113
|
+
}
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Map a forge reading onto the five rows.
|
|
119
|
+
*
|
|
120
|
+
* Never throws and never invents: an input of `null` produces `unread`, which is
|
|
121
|
+
* why the return type has three states rather than two. A gate that reports
|
|
122
|
+
* `false` for something it did not look at is the failure this separation exists
|
|
123
|
+
* to prevent — and it is the failure the `forge-protection-settings` re-scope
|
|
124
|
+
* names, one layer up.
|
|
125
|
+
*/
|
|
126
|
+
export function forgeProtectionRows(reading: ForgeReading): ProtectionRow[] {
|
|
127
|
+
const rows: ProtectionRow[] = [];
|
|
128
|
+
const rulesets = reading.rulesets;
|
|
129
|
+
const branch = reading.defaultBranch;
|
|
130
|
+
|
|
131
|
+
const covering =
|
|
132
|
+
rulesets === null || branch === null
|
|
133
|
+
? null
|
|
134
|
+
: rulesets.filter((rs) => isActiveBranchRuleset(rs) && reachesDefaultBranch(rs, branch));
|
|
135
|
+
|
|
136
|
+
rows.push(
|
|
137
|
+
covering === null
|
|
138
|
+
? {
|
|
139
|
+
id: 'default_branch_protected',
|
|
140
|
+
state: 'unread',
|
|
141
|
+
source: SOURCE.rulesets,
|
|
142
|
+
detail: 'the rulesets were not queried',
|
|
143
|
+
}
|
|
144
|
+
: covering.length > 0
|
|
145
|
+
? {
|
|
146
|
+
id: 'default_branch_protected',
|
|
147
|
+
state: 'satisfied',
|
|
148
|
+
source: SOURCE.rulesets,
|
|
149
|
+
detail: `${String(covering.length)} active branch ruleset(s) cover the default branch`,
|
|
150
|
+
}
|
|
151
|
+
: {
|
|
152
|
+
id: 'default_branch_protected',
|
|
153
|
+
state: 'unsatisfied',
|
|
154
|
+
source: SOURCE.rulesets,
|
|
155
|
+
detail:
|
|
156
|
+
'no active branch ruleset covers the default branch. NOTE: the classic ' +
|
|
157
|
+
'branches/<b>/protection endpoint 404s on a ruleset-protected repository, ' +
|
|
158
|
+
'so a 404 there is not evidence for this row.',
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const contexts = covering === null ? null : covering.flatMap(requiredContexts);
|
|
163
|
+
rows.push(
|
|
164
|
+
contexts === null
|
|
165
|
+
? {
|
|
166
|
+
id: 'required_checks_present',
|
|
167
|
+
state: 'unread',
|
|
168
|
+
source: SOURCE.rulesets,
|
|
169
|
+
detail: 'the rulesets were not queried',
|
|
170
|
+
}
|
|
171
|
+
: contexts.length > 0
|
|
172
|
+
? {
|
|
173
|
+
id: 'required_checks_present',
|
|
174
|
+
state: 'satisfied',
|
|
175
|
+
source: SOURCE.rulesets,
|
|
176
|
+
detail: `${String(contexts.length)} required context(s): ${contexts.join(', ')}`,
|
|
177
|
+
}
|
|
178
|
+
: {
|
|
179
|
+
id: 'required_checks_present',
|
|
180
|
+
state: 'unsatisfied',
|
|
181
|
+
source: SOURCE.rulesets,
|
|
182
|
+
detail: 'no required status check context is configured',
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const noFastForward = covering === null ? null : covering.some((rs) => hasRule(rs, 'non_fast_forward'));
|
|
187
|
+
rows.push(
|
|
188
|
+
noFastForward === null
|
|
189
|
+
? {
|
|
190
|
+
id: 'force_push_disabled',
|
|
191
|
+
state: 'unread',
|
|
192
|
+
source: SOURCE.rulesets,
|
|
193
|
+
detail: 'the rulesets were not queried',
|
|
194
|
+
}
|
|
195
|
+
: {
|
|
196
|
+
id: 'force_push_disabled',
|
|
197
|
+
state: noFastForward ? 'satisfied' : 'unsatisfied',
|
|
198
|
+
source: SOURCE.rulesets,
|
|
199
|
+
detail: noFastForward
|
|
200
|
+
? 'a non_fast_forward rule is active on the default branch'
|
|
201
|
+
: 'no non_fast_forward rule covers the default branch',
|
|
202
|
+
},
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
rows.push(
|
|
206
|
+
reading.allowAutoMerge === null
|
|
207
|
+
? {
|
|
208
|
+
id: 'auto_merge_available',
|
|
209
|
+
state: 'unread',
|
|
210
|
+
source: SOURCE.repo,
|
|
211
|
+
detail: 'the repository record was not queried',
|
|
212
|
+
}
|
|
213
|
+
: {
|
|
214
|
+
id: 'auto_merge_available',
|
|
215
|
+
state: reading.allowAutoMerge ? 'satisfied' : 'unsatisfied',
|
|
216
|
+
source: SOURCE.repo,
|
|
217
|
+
detail: reading.allowAutoMerge
|
|
218
|
+
? 'allow_auto_merge is enabled'
|
|
219
|
+
: 'allow_auto_merge is disabled — merge delivery cannot queue behind checks',
|
|
220
|
+
},
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
rows.push(
|
|
224
|
+
reading.deployRestricted === null
|
|
225
|
+
? {
|
|
226
|
+
id: 'deploy_via_pipeline_only',
|
|
227
|
+
state: 'unread',
|
|
228
|
+
source: SOURCE.environments,
|
|
229
|
+
detail: 'the environments were not queried',
|
|
230
|
+
}
|
|
231
|
+
: {
|
|
232
|
+
id: 'deploy_via_pipeline_only',
|
|
233
|
+
state: reading.deployRestricted ? 'satisfied' : 'unsatisfied',
|
|
234
|
+
source: SOURCE.environments,
|
|
235
|
+
detail: reading.deployRestricted
|
|
236
|
+
? 'every environment restricts its deployment branches'
|
|
237
|
+
: 'at least one environment accepts a deployment from any branch',
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
return rows;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The human ACTION lines for rows that are not satisfied.
|
|
246
|
+
*
|
|
247
|
+
* **Not a halt, and the step says so explicitly**: `doctor` LISTS what is
|
|
248
|
+
* missing and the run continues. An `unread` row produces an action too — "go
|
|
249
|
+
* and read it" is a real instruction, and silently omitting it would make an
|
|
250
|
+
* unmeasured row indistinguishable from a satisfied one, which is the same
|
|
251
|
+
* conflation the three-state row type exists to prevent.
|
|
252
|
+
*/
|
|
253
|
+
export function protectionActions(rows: readonly ProtectionRow[]): string[] {
|
|
254
|
+
return rows
|
|
255
|
+
.filter((r) => r.state !== 'satisfied')
|
|
256
|
+
.map((r) => `${r.id}: ${r.state} — ${r.detail} (source: ${r.source})`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Unconditional bypass actors across the covering rulesets, for reporting. */
|
|
260
|
+
export function unconditionalBypasses(
|
|
261
|
+
rulesets: readonly RulesetDetail[],
|
|
262
|
+
defaultBranch: string,
|
|
263
|
+
): BypassActor[] {
|
|
264
|
+
return rulesets
|
|
265
|
+
.filter((rs) => isActiveBranchRuleset(rs) && reachesDefaultBranch(rs, defaultBranch))
|
|
266
|
+
.flatMap((rs) => (rs.bypass_actors ?? []).filter((a) => a.bypass_mode === 'always'));
|
|
267
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keys under a `hook_manifest.yaml` platform block that are NOT lifecycle slots.
|
|
3
|
+
*
|
|
4
|
+
* One definition, because three consumers had grown their own literal and a
|
|
5
|
+
* fourth key would have had to be added to each of them independently: the
|
|
6
|
+
* manifest lint (which reads an unknown key as an unknown EVENT), the
|
|
7
|
+
* injection-effect report (which counts slots per host), and the install
|
|
8
|
+
* snapshot's drift guard (which asserts every manifest event is bound). Each
|
|
9
|
+
* of those is a different question about the same block, and none of them is
|
|
10
|
+
* asking about metadata.
|
|
11
|
+
*
|
|
12
|
+
* `fallback_only` this package binds nothing on the host; counting it made
|
|
13
|
+
* copilot read one slot against the zero the host table
|
|
14
|
+
* records.
|
|
15
|
+
* `ask` the shape of the host's user-facing question primitive,
|
|
16
|
+
* `native` or `text`. It binds no event, so counting it
|
|
17
|
+
* would add one phantom slot to every host at once.
|
|
18
|
+
*
|
|
19
|
+
* A key added here is a key excluded from every event-shaped reading of the
|
|
20
|
+
* manifest, so add one only when it genuinely carries no binding.
|
|
21
|
+
*/
|
|
22
|
+
export const PLATFORM_METADATA_KEYS: ReadonlySet<string> = new Set(['fallback_only', 'ask']);
|
|
23
|
+
|
|
24
|
+
/** True when a platform-block key describes the host rather than binding an event. */
|
|
25
|
+
export function isPlatformMetadataKey(key: string): boolean {
|
|
26
|
+
return PLATFORM_METADATA_KEYS.has(key);
|
|
27
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `json.dumps` byte-parity helpers — the emit half of the ADR-200 port.
|
|
3
|
+
*
|
|
4
|
+
* The retired Python installer wrote every JSON artefact through
|
|
5
|
+
* `json.dumps(..., ensure_ascii=False)`, and the port's contract is
|
|
6
|
+
* byte-identical output, not merely equivalent JSON. `JSON.stringify` does not
|
|
7
|
+
* satisfy it: it escapes nothing above `\u001f` the same way, renders `NaN` /
|
|
8
|
+
* `Infinity` as `null` rather than as Python's bare tokens, and offers no
|
|
9
|
+
* separator control for the compact NDJSON form. So the three dump shapes the
|
|
10
|
+
* installer needs are written out here, mirroring CPython's encoder:
|
|
11
|
+
*
|
|
12
|
+
* - {@link jsonDumpsIndent} — `json.dumps(data, indent=N, ensure_ascii=False)`
|
|
13
|
+
* with `sort_keys=False` (insertion order preserved, as JS object key order
|
|
14
|
+
* already matches Python dict order for our string keys);
|
|
15
|
+
* - {@link jsonDumpsCompact} — `json.dumps(obj, separators=(",", ":"))`;
|
|
16
|
+
* - {@link jsonStrNoAscii} — the string encoder both build on.
|
|
17
|
+
*
|
|
18
|
+
* Sibling of `src/scripts/install.ts`, which is its only caller. It lives here
|
|
19
|
+
* rather than inline because the dump shapes are pure, total functions of their
|
|
20
|
+
* input with no installer state — and because `install.ts` is far over the
|
|
21
|
+
* 1,500-line source ceiling (`check_source_size_budget`), so a self-contained
|
|
22
|
+
* unit belongs outside it.
|
|
23
|
+
*
|
|
24
|
+
* This module is in the installer's bundled import closure
|
|
25
|
+
* (`npm run build:install-bundle`), so it carries no module-level side effect
|
|
26
|
+
* and no `process.exit` — see `check_installer_import_purity`.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export function jsonStrNoAscii(s: string): string {
|
|
30
|
+
// json.dumps(ensure_ascii=False): escape control chars + " + \, keep >=0x20
|
|
31
|
+
let out = '"';
|
|
32
|
+
for (const ch of s) {
|
|
33
|
+
const code = ch.codePointAt(0) as number;
|
|
34
|
+
switch (ch) {
|
|
35
|
+
case '"':
|
|
36
|
+
out += '\\"';
|
|
37
|
+
break;
|
|
38
|
+
case '\\':
|
|
39
|
+
out += '\\\\';
|
|
40
|
+
break;
|
|
41
|
+
case '\n':
|
|
42
|
+
out += '\\n';
|
|
43
|
+
break;
|
|
44
|
+
case '\r':
|
|
45
|
+
out += '\\r';
|
|
46
|
+
break;
|
|
47
|
+
case '\t':
|
|
48
|
+
out += '\\t';
|
|
49
|
+
break;
|
|
50
|
+
case '\b':
|
|
51
|
+
out += '\\b';
|
|
52
|
+
break;
|
|
53
|
+
case '\f':
|
|
54
|
+
out += '\\f';
|
|
55
|
+
break;
|
|
56
|
+
default:
|
|
57
|
+
if (code < 0x20) {
|
|
58
|
+
out += '\\u' + code.toString(16).padStart(4, '0');
|
|
59
|
+
} else {
|
|
60
|
+
out += ch;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return out + '"';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function _jsonScalar(value: unknown): string | null {
|
|
68
|
+
if (value === null || value === undefined) return 'null';
|
|
69
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
70
|
+
if (typeof value === 'number') {
|
|
71
|
+
if (!Number.isFinite(value)) {
|
|
72
|
+
if (Number.isNaN(value)) return 'NaN';
|
|
73
|
+
return value > 0 ? 'Infinity' : '-Infinity';
|
|
74
|
+
}
|
|
75
|
+
// Our payloads carry only integers; render as-is.
|
|
76
|
+
return String(value);
|
|
77
|
+
}
|
|
78
|
+
if (typeof value === 'string') return jsonStrNoAscii(value);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function _dumpIndent(value: unknown, indent: number, depth: number): string {
|
|
83
|
+
const scalar = _jsonScalar(value);
|
|
84
|
+
if (scalar !== null) return scalar;
|
|
85
|
+
const pad = ' '.repeat(indent * (depth + 1));
|
|
86
|
+
const closePad = ' '.repeat(indent * depth);
|
|
87
|
+
if (Array.isArray(value)) {
|
|
88
|
+
if (value.length === 0) return '[]';
|
|
89
|
+
const items = value.map((v) => pad + _dumpIndent(v, indent, depth + 1));
|
|
90
|
+
return `[\n${items.join(',\n')}\n${closePad}]`;
|
|
91
|
+
}
|
|
92
|
+
if (typeof value === 'object' && value !== null) {
|
|
93
|
+
const obj = value as Record<string, unknown>;
|
|
94
|
+
const keys = Object.keys(obj);
|
|
95
|
+
if (keys.length === 0) return '{}';
|
|
96
|
+
const items = keys.map(
|
|
97
|
+
(k) => `${pad}${jsonStrNoAscii(k)}: ${_dumpIndent(obj[k], indent, depth + 1)}`,
|
|
98
|
+
);
|
|
99
|
+
return `{\n${items.join(',\n')}\n${closePad}}`;
|
|
100
|
+
}
|
|
101
|
+
return jsonStrNoAscii(String(value));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** `json.dumps(data, indent=N, ensure_ascii=False)` (sort_keys=False). */
|
|
105
|
+
export function jsonDumpsIndent(value: unknown, indent: number): string {
|
|
106
|
+
return _dumpIndent(value, indent, 0);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** `json.dumps(obj, separators=(",", ":"))` — compact, ensure_ascii=False here. */
|
|
110
|
+
export function jsonDumpsCompact(value: unknown): string {
|
|
111
|
+
const scalar = _jsonScalar(value);
|
|
112
|
+
if (scalar !== null) return scalar;
|
|
113
|
+
if (Array.isArray(value)) {
|
|
114
|
+
return '[' + value.map((v) => jsonDumpsCompact(v)).join(',') + ']';
|
|
115
|
+
}
|
|
116
|
+
if (typeof value === 'object' && value !== null) {
|
|
117
|
+
const obj = value as Record<string, unknown>;
|
|
118
|
+
return (
|
|
119
|
+
'{' +
|
|
120
|
+
Object.keys(obj)
|
|
121
|
+
.map((k) => `${jsonStrNoAscii(k)}:${jsonDumpsCompact(obj[k])}`)
|
|
122
|
+
.join(',') +
|
|
123
|
+
'}'
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return jsonStrNoAscii(String(value));
|
|
127
|
+
}
|