@event4u/agent-config 16.1.0 → 16.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +237 -248
- package/dist/agent-src/commands/roadmap/process-full.md +1 -1
- package/dist/agent-src/commands/work.md +4 -0
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +4 -8
- package/dist/agent-src/rules/ui-audit-gate.md +1 -2
- package/dist/agent-src/skills/accessibility-auditor/data/aria-patterns.csv +1 -1
- package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +2 -2
- package/dist/agent-src/skills/design-review/SKILL.md +30 -4
- package/dist/agent-src/skills/design-review/references/verification-automation.md +32 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +16 -1
- package/dist/agent-src/skills/existing-ui-audit/references/component-taxonomy-detection.md +56 -0
- package/dist/agent-src/skills/markitdown/SKILL.md +4 -0
- package/dist/agent-src/skills/playwright-testing/SKILL.md +30 -6
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +23 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +30 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
- package/dist/agent-src/templates/scripts/work_engine/state.ts +57 -7
- package/dist/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
- package/dist/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
- package/dist/cli/python/knowledge_ingest.js +1 -1
- package/dist/cli/python/workspace_drive.js +1 -1
- package/dist/cli-delegate/{chunk-S3VFXMTM.js → chunk-4XFP2Y3E.js} +1 -1
- package/dist/cli-delegate/{chunk-HDVVGPQW.js → chunk-7XGBX6ZH.js} +471 -387
- package/dist/cli-delegate/{chunk-GJCBB2UG.js → chunk-FP36SWJD.js} +432 -144
- package/dist/cli-delegate/{chunk-UNCRJBT3.js → chunk-IGD6ERX6.js} +31 -0
- package/dist/cli-delegate/{chunk-FXYUYU4N.js → chunk-JNMVLWLR.js} +42 -1
- package/dist/cli-delegate/{chunk-QJWAUMJW.js → chunk-YJ72UIAK.js} +730 -15
- package/dist/cli-delegate/cmd_conformance.js +902 -782
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +4 -4
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +10 -10
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +6666 -2581
- package/dist/hooks/dispatch.sha256 +1 -0
- package/dist/install/install.mjs +848 -733
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +2747 -1510
- package/dist/server/schemas/settings.js +23 -0
- package/dist/ui/assets/index-Cwq4d0hX.js +23 -0
- package/dist/ui/assets/index-Cyjy8bWO.css +1 -0
- package/dist/ui/index.html +2 -2
- package/docs/contracts/settings-classes.md +6 -4
- package/package.json +4 -4
- package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
- package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
- package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
- package/src/agent-src/templates/scripts/work_engine/state.ts +57 -7
- package/src/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
- package/src/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
- package/src/config/agent-settings.template.yml +29 -0
- package/src/config/ci-local-parity.yml +18 -0
- package/src/config/continuity-surface.json +2 -2
- package/src/config/gate-coverage.yml +234 -3
- package/src/config/gate-violation-baselines.json +7 -3
- package/src/config/hook-latency-budget.json +21 -0
- package/src/config/hook-token-budget.json +2 -2
- package/src/config/metric-registry.yml +114 -0
- package/src/config/placeholder-drift-budget.json +10 -0
- package/src/config/routing-coverage-seed.json +18 -4
- package/src/scripts/_cli/cmd_doctor.ts +5 -19
- package/src/scripts/_cli/doctor_execution.ts +100 -17
- package/src/scripts/_cli/doctor_network_posture.ts +226 -0
- package/src/scripts/_lib/body_portable.ts +102 -0
- package/src/scripts/_lib/cc_transcript.ts +132 -1
- package/src/scripts/_lib/concern_sla_window.ts +200 -0
- package/src/scripts/_lib/forge_protection.ts +114 -0
- package/src/scripts/_lib/forge_reader.ts +558 -0
- package/src/scripts/_lib/host_env_write.ts +111 -0
- package/src/scripts/_lib/llm_proposer_transport.ts +14 -4
- package/src/scripts/_lib/obligations.ts +119 -8
- package/src/scripts/_lib/probe_inputs.ts +271 -0
- package/src/scripts/_lib/read_surface_scan.ts +322 -0
- package/src/scripts/_lib/release_highlights.ts +2 -1
- package/src/scripts/_lib/retrieval_sanitize.ts +77 -16
- package/src/scripts/_lib/shell_write_shapes.ts +108 -0
- package/src/scripts/_lib/structural_hiding.ts +622 -0
- package/src/scripts/_lib/turn_end_refusals.ts +525 -4
- package/src/scripts/_lib/turn_end_transcript.ts +216 -0
- package/src/scripts/_lib/verification_command.ts +324 -0
- package/src/scripts/_lib/verification_evidence.ts +647 -0
- package/src/scripts/_lib/verify_clause.ts +280 -0
- package/src/scripts/ai_team/team_dispatch.ts +4 -0
- package/src/scripts/before_complete_hook.ts +340 -23
- package/src/scripts/bench_hook_latency.ts +412 -5
- package/src/scripts/cache_realization_report.ts +61 -1
- package/src/scripts/check_argument_hint.ts +287 -0
- package/src/scripts/check_claims.ts +125 -1
- package/src/scripts/check_enforcement_coverage.ts +38 -9
- package/src/scripts/check_enforcement_matrix.ts +245 -3
- package/src/scripts/check_finding_dispositions.ts +22 -3
- package/src/scripts/check_generator_sync.ts +71 -0
- package/src/scripts/check_host_docs_digest.ts +786 -0
- package/src/scripts/check_host_format_column.ts +559 -0
- package/src/scripts/check_input_references.ts +228 -0
- package/src/scripts/check_inputs_parity.ts +235 -0
- package/src/scripts/check_kernel_edit_ratified.ts +47 -5
- package/src/scripts/check_kill_switch_table.ts +294 -0
- package/src/scripts/check_placeholder_drift.ts +221 -0
- package/src/scripts/check_read_surface_coverage.ts +515 -0
- package/src/scripts/check_release_holds.ts +2 -1
- package/src/scripts/check_routing_coverage.ts +378 -11
- package/src/scripts/check_static_parity.ts +17 -0
- package/src/scripts/ci_settle.ts +145 -19
- package/src/scripts/closure_scan.ts +115 -1
- package/src/scripts/compile_hook_manifest.ts +60 -16
- package/src/scripts/external_sources_denylist.json +29 -4
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +42 -5
- package/src/scripts/hooks/block_config_weakening.ts +229 -4
- package/src/scripts/hooks/block_kernel_rule_writes.ts +9 -78
- package/src/scripts/hooks/block_plumbing_writes.ts +314 -0
- package/src/scripts/hooks/bundle_integrity.ts +291 -0
- package/src/scripts/hooks/chain_nudge_hook.ts +1 -2
- package/src/scripts/hooks/code_graph_context_hook.ts +1 -2
- package/src/scripts/hooks/comment_discipline_hook.ts +1 -3
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/concern_timings.ts +94 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +1 -2
- package/src/scripts/hooks/design_pass_hook.ts +100 -9
- package/src/scripts/hooks/design_slop_hook.ts +1 -2
- package/src/scripts/hooks/dispatch_hook.ts +25 -19
- package/src/scripts/hooks/edit_shape_hook.ts +1 -2
- package/src/scripts/hooks/end_review_nudge_hook.ts +1 -1
- package/src/scripts/hooks/evidence_independence.ts +1 -2
- package/src/scripts/hooks/exit_codes.ts +128 -0
- package/src/scripts/hooks/host_lowering.json +1 -0
- package/src/scripts/hooks/host_lowering.ts +128 -3
- package/src/scripts/hooks/host_lowering.yaml +262 -46
- package/src/scripts/hooks/host_semantics.ts +30 -2
- package/src/scripts/hooks/injection_budget.ts +9 -2
- package/src/scripts/hooks/interruption_ledger_hook.ts +1 -2
- package/src/scripts/hooks/journal_record_hook.ts +1 -2
- package/src/scripts/hooks/memory_recall_hook.ts +1 -2
- package/src/scripts/hooks/obligation_settle_hook.ts +151 -5
- package/src/scripts/hooks/one_question_per_ask_hook.ts +25 -4
- package/src/scripts/hooks/orchestration_record_hook.ts +1 -2
- package/src/scripts/hooks/permission_gate.ts +1 -2
- package/src/scripts/hooks/push_settle_hook.ts +1 -3
- package/src/scripts/hooks/replay_hook.ts +2 -1
- package/src/scripts/hooks/reread_guard_hook.ts +1 -2
- package/src/scripts/hooks/review_baseline_hook.ts +1 -2
- package/src/scripts/hooks/rtk_wrap_hook.ts +1 -2
- package/src/scripts/hooks/rule_inject_hook.ts +8 -5
- package/src/scripts/hooks/run_continuation_hook.ts +13 -15
- package/src/scripts/hooks/skill_route_hook.ts +1 -2
- package/src/scripts/hooks/source_first_gate_hook.ts +1 -2
- package/src/scripts/hooks/spawn_guard_shadow_hook.ts +1 -2
- package/src/scripts/hooks/subagent_ledger_hook.ts +1 -2
- package/src/scripts/hooks/suggestion_capture_hook.ts +1 -1
- package/src/scripts/hooks/table_fingerprint.ts +49 -0
- package/src/scripts/hooks/telemetry_flush_hook.ts +1 -2
- package/src/scripts/hooks/telemetry_usage_hook.ts +1 -2
- package/src/scripts/hooks/tool_result_bytes_hook.ts +93 -9
- package/src/scripts/hooks/turn_end_gate_hook.ts +337 -324
- package/src/scripts/hooks/ui_route_nudge_hook.ts +1 -2
- package/src/scripts/install.ts +3 -4
- package/src/scripts/lint_deny_text.ts +419 -0
- package/src/scripts/lint_exit_codes.ts +262 -0
- package/src/scripts/lint_hook_manifest.ts +55 -0
- package/src/scripts/lint_roadmap_blockers.ts +78 -12
- package/src/scripts/lint_skill_descriptions.ts +1 -1
- package/src/scripts/lint_skill_scripts_readonly.ts +1 -1
- package/src/scripts/mcp_server/prompts.ts +142 -1
- package/src/scripts/measure_skill_ranker_baseline.ts +305 -51
- package/src/scripts/measure_turn_end_gate.ts +193 -0
- package/src/scripts/memory_lookup.ts +1 -1
- package/src/scripts/prepack-check.mjs +53 -0
- package/src/scripts/release_publication.ts +13 -1
- package/src/scripts/report_invocation_surface.ts +383 -0
- package/src/scripts/report_obligation_carriers.ts +699 -0
- package/src/scripts/report_obligation_writer_split.ts +236 -0
- package/src/scripts/report_skill_menu_census.ts +208 -0
- package/src/scripts/report_standing_payload_by_host.ts +1 -1
- package/src/scripts/roadmap_verify_share.ts +213 -0
- package/src/scripts/schemas/command.schema.json +44 -0
- package/src/scripts/schemas/review-findings.schema.json +4 -0
- package/src/scripts/schemas/skill.schema.json +44 -0
- package/src/scripts/self_review_gate.ts +359 -10
- package/src/scripts/semantic_parity_census.ts +314 -0
- package/src/scripts/session_eol_report.ts +54 -1
- package/src/scripts/sweep_dead_scan_roots.ts +1 -1
- package/src/scripts/trigger_eval_grandfather.json +0 -2
- package/src/scripts/trigger_eval_rotation.ts +122 -27
- package/src/scripts/ui_conformance_probe.ts +156 -4
- package/src/scripts/update_prices.ts +85 -3
- package/src/scripts/write_bundle_digest.mjs +74 -0
- package/src/server/schemas/settings.ts +31 -0
- package/src/templates/consumer-settings/ONBOARDING.md +1 -0
- package/dist/ui/assets/index-DH1xc62v.js +0 -29
- package/dist/ui/assets/index-jSfMW8_t.css +0 -1
- /package/dist/cli-delegate/{chunk-YUM5KURG.js → chunk-7RQP4UDH.js} +0 -0
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
39
|
import { load_anthropic_key } from '../ai_council/clients.js';
|
|
40
|
+
import { sanitize_text } from './retrieval_sanitize.js';
|
|
40
41
|
import type {
|
|
41
42
|
GenerationRequest,
|
|
42
43
|
GenerationResult,
|
|
@@ -140,10 +141,19 @@ export function anthropicGenerator(keyPath: string | null = null): TextGenerator
|
|
|
140
141
|
);
|
|
141
142
|
}
|
|
142
143
|
const data = (await resp.json()) as AnthropicResponse;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
// Sanitize AT THE TRANSPORT, which is the choke point and one call site.
|
|
145
|
+
// A flag every caller has to remember is the instruction-only
|
|
146
|
+
// enforcement this package diagnoses in itself, and a partially
|
|
147
|
+
// sanitized world — some callers filtered, some not — is worse than
|
|
148
|
+
// either end state. The floor is a no-op on ordinary text, so what
|
|
149
|
+
// changes here is only a response that carries a hidden-instruction
|
|
150
|
+
// vector; see `docs/contracts/retrieval-read-surfaces.md`.
|
|
151
|
+
const text = sanitize_text(
|
|
152
|
+
(data.content ?? [])
|
|
153
|
+
.filter((b) => b.type === 'text')
|
|
154
|
+
.map((b) => b.text ?? '')
|
|
155
|
+
.join(''),
|
|
156
|
+
);
|
|
147
157
|
// An empty body is returned AS an empty body rather than repaired here.
|
|
148
158
|
// The arm's own output contract refuses it and classifies the refusal;
|
|
149
159
|
// repairing it in the transport would move a decision into the layer
|
|
@@ -44,7 +44,9 @@ import {
|
|
|
44
44
|
session_state_file,
|
|
45
45
|
update_json_under_lock,
|
|
46
46
|
} from '../hooks/state_io.js';
|
|
47
|
+
import { manifest_path } from './installed_tools.js';
|
|
47
48
|
import { type EnforcementClass, is_enforcement_class } from './obligation_frequency.js';
|
|
49
|
+
import { hasSentinel } from './repo_root.js';
|
|
48
50
|
|
|
49
51
|
/**
|
|
50
52
|
* Directory holding one ledger file per session.
|
|
@@ -61,6 +63,77 @@ export function statePathFor(session_id: string): string {
|
|
|
61
63
|
return session_state_file(OBLIGATION_STATE_DIR, session_id);
|
|
62
64
|
}
|
|
63
65
|
|
|
66
|
+
/**
|
|
67
|
+
* WHICH KIND OF TREE WROTE A ROW — the whole vocabulary, and nothing else.
|
|
68
|
+
*
|
|
69
|
+
* `obligation-settle-shadow-bar` is read off `agents/runtime/state/obligations/`,
|
|
70
|
+
* which is one machine's gitignored runtime state. That scope bound lives in the
|
|
71
|
+
* claim's prose and in a roadmap paragraph, and in nothing a reader of the rows
|
|
72
|
+
* can compute. A reading that clears the pre-registered floor would therefore be
|
|
73
|
+
* quotable as evidence about a population nobody separated. This field makes the
|
|
74
|
+
* separation a property of the DATA, so the split is computable from the ledger
|
|
75
|
+
* alone rather than asserted beside it.
|
|
76
|
+
*
|
|
77
|
+
* THREE VALUES, CLOSED, AND A UNION OF LITERALS ON PURPOSE. The record gains one
|
|
78
|
+
* enumerated field and no `payload`, `notes` or `extra` slot, so the privacy
|
|
79
|
+
* property is the SHAPE and not a scrubber somebody has to keep correct — the
|
|
80
|
+
* same by-construction posture `artifact-engagement-recording` states for the
|
|
81
|
+
* telemetry event. A type that cannot hold a path cannot leak one.
|
|
82
|
+
*
|
|
83
|
+
* WHAT IT DELIBERATELY IS NOT. Not a path, not a username, not a hostname, not a
|
|
84
|
+
* machine id, not a git remote. The obvious implementation of "which tree wrote
|
|
85
|
+
* this" is the resolved root's filesystem path, which on a developer machine
|
|
86
|
+
* carries an account name — and because the bar is read by QUOTING rows, one such
|
|
87
|
+
* field would make every row already written unquotable rather than scrubbable.
|
|
88
|
+
* So the field carries a ROLE: what kind of tree this is, never which one.
|
|
89
|
+
*/
|
|
90
|
+
export type WriterRole =
|
|
91
|
+
/** The tree carries this package's own `package.json` sentinel — a maintainer checkout. */
|
|
92
|
+
| 'package'
|
|
93
|
+
/** Not the package, but an install manifest is present — a consumer project. */
|
|
94
|
+
| 'consumer'
|
|
95
|
+
/** Neither marker resolved. An honest absence, never a default. */
|
|
96
|
+
| 'unknown';
|
|
97
|
+
|
|
98
|
+
/** The closed set, in one place, so a reader and a writer cannot disagree. */
|
|
99
|
+
export const WRITER_ROLES: readonly WriterRole[] = ['package', 'consumer', 'unknown'];
|
|
100
|
+
|
|
101
|
+
/** Narrowing guard for a value read back off disk. */
|
|
102
|
+
export function isWriterRole(value: unknown): value is WriterRole {
|
|
103
|
+
return typeof value === 'string' && (WRITER_ROLES as readonly string[]).includes(value);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Classify the root this ledger lives under.
|
|
108
|
+
*
|
|
109
|
+
* ORDER IS THE CONTRACT. `package` is tested FIRST, because a maintainer checkout
|
|
110
|
+
* routinely also carries the install artifacts a consumer has — this repository
|
|
111
|
+
* installs itself — and a consumer-first test would label the maintainer tree
|
|
112
|
+
* `consumer` on exactly the machine the bar is being read from. The sentinel is
|
|
113
|
+
* `hasSentinel` from `repo_root.ts` rather than a second `package.json` reader:
|
|
114
|
+
* the sentinel name lives in one place or it drifts.
|
|
115
|
+
*
|
|
116
|
+
* `consumer` keys on the INSTALL MANIFEST (`agents/installed-tools.lock`, via the
|
|
117
|
+
* canonical `manifest_path` so the `AGENT_CONFIG_INSTALLED_TOOLS` override is
|
|
118
|
+
* honoured) and not on `.agent-settings.yml`. The settings file is something a
|
|
119
|
+
* human writes by hand; the lock is written by the installer and by nothing else.
|
|
120
|
+
* A weaker marker would buy coverage by guessing, which is the one thing this
|
|
121
|
+
* field may not do.
|
|
122
|
+
*
|
|
123
|
+
* NEVER THROWS. A probe that cannot stat returns `unknown`, which is a real
|
|
124
|
+
* answer — the ledger's contract is that an instrument missing a reading is
|
|
125
|
+
* cheaper than a refused turn, and a classifier is no exception.
|
|
126
|
+
*/
|
|
127
|
+
export function resolveWriterRole(root: string): WriterRole {
|
|
128
|
+
try {
|
|
129
|
+
if (hasSentinel(root)) return 'package';
|
|
130
|
+
if (fs.existsSync(manifest_path(root))) return 'consumer';
|
|
131
|
+
} catch {
|
|
132
|
+
return 'unknown';
|
|
133
|
+
}
|
|
134
|
+
return 'unknown';
|
|
135
|
+
}
|
|
136
|
+
|
|
64
137
|
/**
|
|
65
138
|
* One rule delivered into this session, with the class its frontmatter declares.
|
|
66
139
|
*
|
|
@@ -75,8 +148,21 @@ export interface DeliveredRow {
|
|
|
75
148
|
readonly cls: EnforcementClass;
|
|
76
149
|
/** ISO-8601, second precision. */
|
|
77
150
|
readonly at: string;
|
|
151
|
+
/** Which kind of tree wrote this. Stamped here, never supplied — see {@link WriterInput}. */
|
|
152
|
+
readonly writer: WriterRole;
|
|
78
153
|
}
|
|
79
154
|
|
|
155
|
+
/**
|
|
156
|
+
* What a CALLER offers, for every row type: the stored shape minus its writer.
|
|
157
|
+
*
|
|
158
|
+
* The writer is stamped by the append functions from the `root` they were
|
|
159
|
+
* already given, and is deliberately not part of any caller's input. A caller
|
|
160
|
+
* that could supply it could supply the wrong one — and a provenance field a
|
|
161
|
+
* caller can set is a provenance field, not provenance. Making it unsettable is
|
|
162
|
+
* cheaper than any check that it was set correctly.
|
|
163
|
+
*/
|
|
164
|
+
export type WriterInput<T> = Omit<T, 'writer'>;
|
|
165
|
+
|
|
80
166
|
/**
|
|
81
167
|
* One obligation discharged this session.
|
|
82
168
|
*
|
|
@@ -92,6 +178,8 @@ export interface DischargeRow {
|
|
|
92
178
|
/** What observed it — a concern name, so a reader can go and check. */
|
|
93
179
|
readonly by: string;
|
|
94
180
|
readonly at: string;
|
|
181
|
+
/** Which kind of tree wrote this. Stamped, never supplied. */
|
|
182
|
+
readonly writer: WriterRole;
|
|
95
183
|
}
|
|
96
184
|
|
|
97
185
|
/**
|
|
@@ -109,6 +197,14 @@ export interface ShadowRow {
|
|
|
109
197
|
readonly attempt: number;
|
|
110
198
|
/** Always true today: a row is only written when the detector would refuse. */
|
|
111
199
|
readonly would_refuse: boolean;
|
|
200
|
+
/**
|
|
201
|
+
* Which kind of tree wrote this. Stamped, never supplied.
|
|
202
|
+
*
|
|
203
|
+
* The shadow array is the one the pre-registered bar actually counts, so
|
|
204
|
+
* this is the field that turns "the corpus is one machine" from a sentence
|
|
205
|
+
* in a claim into something a reader can compute off the rows.
|
|
206
|
+
*/
|
|
207
|
+
readonly writer: WriterRole;
|
|
112
208
|
}
|
|
113
209
|
|
|
114
210
|
/** The on-disk shape of one session's ledger. */
|
|
@@ -125,10 +221,11 @@ interface SerialisedRow {
|
|
|
125
221
|
rule: string;
|
|
126
222
|
class: string;
|
|
127
223
|
at: string;
|
|
224
|
+
writer: WriterRole;
|
|
128
225
|
}
|
|
129
226
|
|
|
130
227
|
function serialise(row: DeliveredRow): SerialisedRow {
|
|
131
|
-
return { rule: row.rule, class: row.cls, at: row.at };
|
|
228
|
+
return { rule: row.rule, class: row.cls, at: row.at, writer: row.writer };
|
|
132
229
|
}
|
|
133
230
|
|
|
134
231
|
/**
|
|
@@ -139,6 +236,16 @@ function serialise(row: DeliveredRow): SerialisedRow {
|
|
|
139
236
|
* whose class is outside the closed set must not enter the ledger wearing a
|
|
140
237
|
* value the vocabulary denies — `is_enforcement_class` is the gate, so a
|
|
141
238
|
* future widening of the schema cannot leak in through the state file.
|
|
239
|
+
*
|
|
240
|
+
* THE WRITER IS TREATED THE OTHER WAY ROUND, and the asymmetry is deliberate. A
|
|
241
|
+
* row whose CLASS is outside the vocabulary is dropped, because the class is the
|
|
242
|
+
* row's subject matter and a row wearing a denied class is a false statement
|
|
243
|
+
* about what was delivered. A row whose WRITER is absent or unrecognised is KEPT
|
|
244
|
+
* and reads `unknown`, because the writer is provenance ABOUT a delivery that
|
|
245
|
+
* genuinely happened — dropping the row would destroy a real record to punish a
|
|
246
|
+
* missing label, and every row written before this field existed is exactly that
|
|
247
|
+
* case. `unknown` is the honest reading of both, which is why one value covers
|
|
248
|
+
* absent and unresolvable alike.
|
|
142
249
|
*/
|
|
143
250
|
export function parseRows(loaded: unknown): DeliveredRow[] {
|
|
144
251
|
if (typeof loaded !== 'object' || loaded === null) return [];
|
|
@@ -154,7 +261,8 @@ export function parseRows(loaded: unknown): DeliveredRow[] {
|
|
|
154
261
|
if (typeof rule !== 'string' || rule === '') continue;
|
|
155
262
|
if (typeof at !== 'string' || at === '') continue;
|
|
156
263
|
if (!is_enforcement_class(cls)) continue;
|
|
157
|
-
|
|
264
|
+
const writer = e['writer'];
|
|
265
|
+
out.push({ rule, cls, at, writer: isWriterRole(writer) ? writer : 'unknown' });
|
|
158
266
|
}
|
|
159
267
|
return out;
|
|
160
268
|
}
|
|
@@ -185,13 +293,14 @@ export function stamp(now: Date = new Date()): string {
|
|
|
185
293
|
export function appendDelivered(
|
|
186
294
|
root: string,
|
|
187
295
|
session_id: string,
|
|
188
|
-
rows: readonly DeliveredRow[],
|
|
296
|
+
rows: readonly WriterInput<DeliveredRow>[],
|
|
189
297
|
): number {
|
|
190
298
|
if (rows.length === 0) return 0;
|
|
191
299
|
if (is_replay_mode()) return 0;
|
|
192
300
|
if (session_id.trim() === '') return 0;
|
|
193
301
|
|
|
194
302
|
const target = path.join(root, statePathFor(session_id));
|
|
303
|
+
const writer = resolveWriterRole(root);
|
|
195
304
|
let added = 0;
|
|
196
305
|
try {
|
|
197
306
|
update_json_under_lock<LedgerFile>(target, (loaded) => {
|
|
@@ -201,7 +310,7 @@ export function appendDelivered(
|
|
|
201
310
|
for (const row of rows) {
|
|
202
311
|
if (seen.has(row.rule)) continue;
|
|
203
312
|
seen.add(row.rule);
|
|
204
|
-
merged.push(serialise(row));
|
|
313
|
+
merged.push(serialise({ ...row, writer }));
|
|
205
314
|
added += 1;
|
|
206
315
|
}
|
|
207
316
|
if (added === 0) return null; // deliberate no-write
|
|
@@ -245,24 +354,25 @@ function carryOver(loaded: Partial<LedgerFile>): Pick<LedgerFile, 'discharged' |
|
|
|
245
354
|
export function appendDischarge(
|
|
246
355
|
root: string,
|
|
247
356
|
session_id: string,
|
|
248
|
-
rows: readonly DischargeRow[],
|
|
357
|
+
rows: readonly WriterInput<DischargeRow>[],
|
|
249
358
|
): number {
|
|
250
359
|
if (rows.length === 0) return 0;
|
|
251
360
|
if (is_replay_mode()) return 0;
|
|
252
361
|
if (session_id.trim() === '') return 0;
|
|
253
362
|
|
|
254
363
|
const target = path.join(root, statePathFor(session_id));
|
|
364
|
+
const writer = resolveWriterRole(root);
|
|
255
365
|
let added = 0;
|
|
256
366
|
try {
|
|
257
367
|
update_json_under_lock<LedgerFile>(target, (loaded) => {
|
|
258
368
|
const existing = Array.isArray(loaded.discharged) ? loaded.discharged : [];
|
|
259
|
-
const key = (r: DischargeRow): string => `${r.rule}::${r.by}`;
|
|
369
|
+
const key = (r: WriterInput<DischargeRow>): string => `${r.rule}::${r.by}`;
|
|
260
370
|
const seen = new Set(existing.map(key));
|
|
261
371
|
const merged = [...existing];
|
|
262
372
|
for (const row of rows) {
|
|
263
373
|
if (seen.has(key(row))) continue;
|
|
264
374
|
seen.add(key(row));
|
|
265
|
-
merged.push(row);
|
|
375
|
+
merged.push({ ...row, writer });
|
|
266
376
|
added += 1;
|
|
267
377
|
}
|
|
268
378
|
if (added === 0) return null;
|
|
@@ -320,6 +430,7 @@ export function appendShadow(
|
|
|
320
430
|
|
|
321
431
|
const fingerprint = [...missing].sort().join('::');
|
|
322
432
|
const target = path.join(root, statePathFor(session_id));
|
|
433
|
+
const writer = resolveWriterRole(root);
|
|
323
434
|
let attempt = 0;
|
|
324
435
|
try {
|
|
325
436
|
update_json_under_lock<LedgerFile>(target, (loaded) => {
|
|
@@ -334,7 +445,7 @@ export function appendShadow(
|
|
|
334
445
|
discharged: Array.isArray(loaded.discharged) ? loaded.discharged : [],
|
|
335
446
|
shadow: [
|
|
336
447
|
...existing,
|
|
337
|
-
{ at: now, missing: [...missing], attempt, would_refuse: true },
|
|
448
|
+
{ at: now, missing: [...missing], attempt, would_refuse: true, writer },
|
|
338
449
|
],
|
|
339
450
|
};
|
|
340
451
|
});
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `probe_inputs` — what the UI-conformance probe read, in a form its reader can
|
|
3
|
+
* re-check without starting a browser.
|
|
4
|
+
*
|
|
5
|
+
* `ui-conformance.json` recorded `generated_at` and nothing about its inputs, so
|
|
6
|
+
* a probe run before the last UI edit was indistinguishable from one run after
|
|
7
|
+
* it. This module is the shared vocabulary that closes that: the probe writes an
|
|
8
|
+
* `inputs` block with a content digest per input, and the design-pass reader
|
|
9
|
+
* recomputes those digests and compares. ONE definition, imported by both sides,
|
|
10
|
+
* because a producer and a consumer with separate copies of a hash rule agree
|
|
11
|
+
* only until one of them is edited.
|
|
12
|
+
*
|
|
13
|
+
* DIGESTS, NOT MTIMES, AND THAT IS THE WHOLE DECISION (D1). The reader already
|
|
14
|
+
* applies an mtime rule to the ui-audit artefact, and reusing it here was the
|
|
15
|
+
* obvious move. It answers the wrong question: an mtime moves on a checkout, a
|
|
16
|
+
* `cp -p`-less copy, or a bare `touch`, so it would report movement on turns
|
|
17
|
+
* where no byte changed — and a reader who learns to ignore a false stale has
|
|
18
|
+
* learned to ignore a real one. A content digest reports exactly what is being
|
|
19
|
+
* asked: are the bytes the probe saw still the bytes on disk?
|
|
20
|
+
*
|
|
21
|
+
* THE TARGET DIGEST IS A TREE DIGEST, and this is the one place the roadmap's
|
|
22
|
+
* own text needed correcting. A UI surface is an entry document plus the styles
|
|
23
|
+
* and scripts beside it; a digest of `index.html` alone would be blind to a CSS
|
|
24
|
+
* edit, which is the single most common UI change there is — and the roadmap's
|
|
25
|
+
* own verify line for step 1.1 names exactly that case. So the record covers the
|
|
26
|
+
* directory the entry document sits in. It costs a small recursive read the
|
|
27
|
+
* probe does not otherwise perform (the browser, not Node, opens `styles.css`),
|
|
28
|
+
* which is a real cost the roadmap's Risk 3 mitigation did not anticipate. It is
|
|
29
|
+
* bounded: a few small text files against a run that launches Chromium and
|
|
30
|
+
* reloads the page dozens of times per node.
|
|
31
|
+
*
|
|
32
|
+
* NO BROWSER IS REACHABLE FROM HERE. This module imports `node:crypto`,
|
|
33
|
+
* `node:fs` and `node:path` and nothing else, which is what lets the hook decide
|
|
34
|
+
* staleness on a host with no browser binaries installed.
|
|
35
|
+
*/
|
|
36
|
+
import { createHash } from 'node:crypto';
|
|
37
|
+
import fs from 'node:fs';
|
|
38
|
+
import path from 'node:path';
|
|
39
|
+
|
|
40
|
+
/** Schema of the `inputs` block. A reader that does not know it says unknown. */
|
|
41
|
+
export const INPUTS_SCHEMA = 'ui-conformance-inputs/v1';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The probe build that wrote the block. Bumped when a change to the probe makes
|
|
45
|
+
* an older artefact's findings incomparable with a newer run's; a reader seeing
|
|
46
|
+
* a value it does not know reports unknown rather than guessing.
|
|
47
|
+
*/
|
|
48
|
+
export const PROBE_VERSION = 'ui-conformance-probe/v1';
|
|
49
|
+
|
|
50
|
+
/** The sibling file the probe reads for declared, approved deviations. */
|
|
51
|
+
export const DECLARATIONS_BASENAME = 'conformance.declared.json';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Bound on a tree digest. A surface directory is a handful of files; anything
|
|
55
|
+
* past this is a checkout root someone pointed the probe at by mistake, and
|
|
56
|
+
* hashing it would cost more than the probe run. Exceeding it is recorded as
|
|
57
|
+
* absent with a reason — never silently truncated, which would produce a digest
|
|
58
|
+
* that looks authoritative and covers an arbitrary subset.
|
|
59
|
+
*/
|
|
60
|
+
export const MAX_TREE_FILES = 2000;
|
|
61
|
+
|
|
62
|
+
/** Directories never worth digesting, and ruinous to walk. */
|
|
63
|
+
const SKIP_DIRS = new Set(['node_modules', '.git']);
|
|
64
|
+
|
|
65
|
+
export const INPUT_KEYS = ['target', 'reference', 'declarations'] as const;
|
|
66
|
+
export type InputKey = (typeof INPUT_KEYS)[number];
|
|
67
|
+
|
|
68
|
+
export interface InputRecord {
|
|
69
|
+
/**
|
|
70
|
+
* What was digested — a directory for `tree`, a file for `file`. `null` only
|
|
71
|
+
* when no path was supplied at all, which is itself recorded rather than
|
|
72
|
+
* omitted.
|
|
73
|
+
*/
|
|
74
|
+
path: string | null;
|
|
75
|
+
kind: 'tree' | 'file';
|
|
76
|
+
/** `absent` carries a reason and a null digest. Never an omitted key. */
|
|
77
|
+
state: 'read' | 'absent';
|
|
78
|
+
/** `sha256:<hex>` when read, `null` when absent. */
|
|
79
|
+
digest: string | null;
|
|
80
|
+
/** Files folded into a tree digest. Absent on a `file` record. */
|
|
81
|
+
files?: number;
|
|
82
|
+
reason?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ProbeInputs {
|
|
86
|
+
schema: string;
|
|
87
|
+
probe: string;
|
|
88
|
+
target: InputRecord;
|
|
89
|
+
reference: InputRecord;
|
|
90
|
+
declarations: InputRecord;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const sha256 = (data: Buffer | string): string => `sha256:${createHash('sha256').update(data).digest('hex')}`;
|
|
94
|
+
|
|
95
|
+
const why = (err: unknown): string => {
|
|
96
|
+
const code = (err as NodeJS.ErrnoException | undefined)?.code;
|
|
97
|
+
return code ? String(code) : String(err);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The ONE definition of where a target's declarations file lives. The probe's
|
|
102
|
+
* CLI computed this inline; a second copy here would be a rule that agrees with
|
|
103
|
+
* the probe only until one of the two is edited.
|
|
104
|
+
*/
|
|
105
|
+
export function declarationsPathFor(targetEntryFile: string): string {
|
|
106
|
+
return path.join(path.dirname(targetEntryFile), DECLARATIONS_BASENAME);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Digest one file. Never throws — an unreadable input is a recorded fact. */
|
|
110
|
+
export function digestFile(file: string | null, whenNull = 'no path was supplied'): InputRecord {
|
|
111
|
+
if (!file) return { path: null, kind: 'file', state: 'absent', digest: null, reason: whenNull };
|
|
112
|
+
try {
|
|
113
|
+
return { path: file, kind: 'file', state: 'read', digest: sha256(fs.readFileSync(file)) };
|
|
114
|
+
} catch (err) {
|
|
115
|
+
return { path: file, kind: 'file', state: 'absent', digest: null, reason: `unreadable: ${why(err)}` };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Digest a directory as one value: every file under it, sorted by relative path,
|
|
121
|
+
* folded path-and-content into a single hash. Sorted because a directory listing
|
|
122
|
+
* order is a filesystem detail, and a digest that moved with it would report
|
|
123
|
+
* movement nobody made.
|
|
124
|
+
*
|
|
125
|
+
* `exclude` drops names recorded as inputs in their own right — the declarations
|
|
126
|
+
* file — so an edit to it moves exactly one digest rather than two.
|
|
127
|
+
*/
|
|
128
|
+
export function digestTree(
|
|
129
|
+
dir: string | null,
|
|
130
|
+
exclude: readonly string[] = [],
|
|
131
|
+
whenNull = 'no path was supplied',
|
|
132
|
+
): InputRecord {
|
|
133
|
+
if (!dir) return { path: null, kind: 'tree', state: 'absent', digest: null, reason: whenNull };
|
|
134
|
+
const skip = new Set(exclude);
|
|
135
|
+
const found: string[] = [];
|
|
136
|
+
|
|
137
|
+
const walk = (rel: string): string | null => {
|
|
138
|
+
let entries: fs.Dirent[];
|
|
139
|
+
try {
|
|
140
|
+
entries = fs.readdirSync(path.join(dir, rel), { withFileTypes: true });
|
|
141
|
+
} catch (err) {
|
|
142
|
+
return `unreadable: ${why(err)}`;
|
|
143
|
+
}
|
|
144
|
+
for (const e of [...entries].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))) {
|
|
145
|
+
const child = rel ? `${rel}/${e.name}` : e.name;
|
|
146
|
+
if (rel === '' && skip.has(e.name)) continue;
|
|
147
|
+
if (e.isDirectory()) {
|
|
148
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
149
|
+
const failed = walk(child);
|
|
150
|
+
if (failed) return failed;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (!e.isFile()) continue;
|
|
154
|
+
found.push(child);
|
|
155
|
+
if (found.length > MAX_TREE_FILES) {
|
|
156
|
+
return `directory holds more than ${MAX_TREE_FILES} files — too large to digest`;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const failure = walk('');
|
|
163
|
+
if (failure) return { path: dir, kind: 'tree', state: 'absent', digest: null, reason: failure };
|
|
164
|
+
|
|
165
|
+
const h = createHash('sha256');
|
|
166
|
+
for (const rel of found.sort()) {
|
|
167
|
+
let body: Buffer;
|
|
168
|
+
try {
|
|
169
|
+
body = fs.readFileSync(path.join(dir, rel));
|
|
170
|
+
} catch (err) {
|
|
171
|
+
return { path: dir, kind: 'tree', state: 'absent', digest: null, reason: `unreadable: ${why(err)}` };
|
|
172
|
+
}
|
|
173
|
+
// The path is folded in alongside the bytes, so a rename moves the digest
|
|
174
|
+
// even when the content is byte-identical.
|
|
175
|
+
h.update(rel).update('\0').update(body).update('\0');
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
path: dir,
|
|
179
|
+
kind: 'tree',
|
|
180
|
+
state: 'read',
|
|
181
|
+
digest: `sha256:${h.digest('hex')}`,
|
|
182
|
+
files: found.length,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The block the probe writes beside `generated_at`. A target or reference is
|
|
188
|
+
* digested as the surface directory it belongs to; the declarations file is
|
|
189
|
+
* digested on its own.
|
|
190
|
+
*/
|
|
191
|
+
export function collectInputs(targetEntryFile: string | null, referenceEntryFile: string | null): ProbeInputs {
|
|
192
|
+
const dirOf = (f: string | null): string | null => (f ? path.dirname(f) : null);
|
|
193
|
+
return {
|
|
194
|
+
schema: INPUTS_SCHEMA,
|
|
195
|
+
probe: PROBE_VERSION,
|
|
196
|
+
target: digestTree(dirOf(targetEntryFile), [DECLARATIONS_BASENAME], 'no --target was supplied'),
|
|
197
|
+
reference: digestTree(
|
|
198
|
+
dirOf(referenceEntryFile),
|
|
199
|
+
[DECLARATIONS_BASENAME],
|
|
200
|
+
'no --reference was given; nothing was compared against',
|
|
201
|
+
),
|
|
202
|
+
declarations: targetEntryFile
|
|
203
|
+
? digestFile(declarationsPathFor(targetEntryFile))
|
|
204
|
+
: digestFile(null, 'no --target was supplied, so no declarations file was looked for'),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export type InputComparison =
|
|
209
|
+
| { verdict: 'unknown'; reason: string; moved: readonly InputKey[] }
|
|
210
|
+
| { verdict: 'unchanged'; moved: readonly InputKey[] }
|
|
211
|
+
| { verdict: 'moved'; moved: readonly InputKey[] };
|
|
212
|
+
|
|
213
|
+
const isRecord = (v: unknown): v is InputRecord =>
|
|
214
|
+
typeof v === 'object' && v !== null && 'state' in (v as object) && 'kind' in (v as object);
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Re-read each recorded input and say whether it moved.
|
|
218
|
+
*
|
|
219
|
+
* THREE verdicts, and `unknown` is not a hedge. An artefact written before the
|
|
220
|
+
* `inputs` block existed has nothing to compare, and calling that `moved` would
|
|
221
|
+
* report stale for every pre-existing run on the first turn after this change —
|
|
222
|
+
* Risk 1 of the roadmap. It is a version skew, not evidence of movement, and it
|
|
223
|
+
* is equally not evidence of freshness.
|
|
224
|
+
*
|
|
225
|
+
* `resolve` maps a recorded path to this host's filesystem; the caller owns that
|
|
226
|
+
* because the probe records the path it was given and only the reader knows the
|
|
227
|
+
* root it is reading from.
|
|
228
|
+
*/
|
|
229
|
+
export function compareInputs(recorded: unknown, resolve: (p: string) => string): InputComparison {
|
|
230
|
+
if (typeof recorded !== 'object' || recorded === null) {
|
|
231
|
+
return {
|
|
232
|
+
verdict: 'unknown',
|
|
233
|
+
moved: [],
|
|
234
|
+
reason: 'the artefact carries no inputs block — it predates the input digests',
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const r = recorded as Partial<ProbeInputs>;
|
|
238
|
+
if (r.schema !== INPUTS_SCHEMA) {
|
|
239
|
+
return {
|
|
240
|
+
verdict: 'unknown',
|
|
241
|
+
moved: [],
|
|
242
|
+
reason: `inputs schema ${String(r.schema)} is not ${INPUTS_SCHEMA} — this reader cannot compare it`,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
if (r.probe !== PROBE_VERSION) {
|
|
246
|
+
return {
|
|
247
|
+
verdict: 'unknown',
|
|
248
|
+
moved: [],
|
|
249
|
+
reason: `written by ${String(r.probe)}, read by ${PROBE_VERSION} — findings across builds are not comparable`,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const moved: InputKey[] = [];
|
|
254
|
+
for (const key of INPUT_KEYS) {
|
|
255
|
+
const rec = r[key];
|
|
256
|
+
if (!isRecord(rec)) {
|
|
257
|
+
return {
|
|
258
|
+
verdict: 'unknown',
|
|
259
|
+
moved: [],
|
|
260
|
+
reason: `the inputs block carries no usable record for ${key}`,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
// An input that was never supplied has nothing to move to or from.
|
|
264
|
+
if (rec.path === null) continue;
|
|
265
|
+
const here = rec.kind === 'tree'
|
|
266
|
+
? digestTree(resolve(rec.path), [DECLARATIONS_BASENAME])
|
|
267
|
+
: digestFile(resolve(rec.path));
|
|
268
|
+
if (here.state !== rec.state || here.digest !== rec.digest) moved.push(key);
|
|
269
|
+
}
|
|
270
|
+
return moved.length ? { verdict: 'moved', moved } : { verdict: 'unchanged', moved: [] };
|
|
271
|
+
}
|