@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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@event4u/agent-config",
|
|
3
3
|
"mcpName": "io.github.event4u-app/agent-config",
|
|
4
|
-
"version": "16.
|
|
4
|
+
"version": "16.2.0",
|
|
5
5
|
"description": "Governed skills, rules and commands for AI coding tools (Claude Code, Cursor, Windsurf, Copilot) — every claim machine-checked, including the counts in these badges.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"private": false,
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"scripts": {
|
|
81
81
|
"build:cli": "tsc -p tsconfig.json && chmod +x dist/cli/agent-config.js",
|
|
82
82
|
"build:install-bundle": "esbuild src/scripts/install.ts --bundle --platform=node --format=esm --target=node20 --define:__AGENT_CONFIG_BUNDLE__=true --outfile=dist/install/install.mjs --banner:js=\"import { createRequire as __acCreateRequire } from 'node:module'; const require = globalThis.require ?? __acCreateRequire(import.meta.url);\"",
|
|
83
|
-
"build:hooks": "esbuild src/scripts/hooks/dispatch_entry.ts --bundle --platform=node --format=esm --target=node20 --define:__AGENT_CONFIG_BUNDLE__=true --outfile=dist/hooks/dispatch.js --banner:js=\"import { createRequire as __acCreateRequire } from 'node:module'; const require = globalThis.require ?? __acCreateRequire(import.meta.url); const __acPath = require('node:path'); const __acFs = require('node:fs'); const __acUrl = require('node:url'); const __acReal = (p) => { try { return __acFs.realpathSync(p); } catch { return p; } }; const __acSelf = __acReal(__acUrl.fileURLToPath(import.meta.url)); if (process.argv[1] !== undefined && __acReal(__acPath.resolve(process.argv[1])) === __acSelf) { globalThis.__AC_HOOKS_BUNDLE_DIRECT = true; process.argv = [process.argv[0], __acSelf + '.__direct__', ...process.argv.slice(2)]; }\"",
|
|
83
|
+
"build:hooks": "esbuild src/scripts/hooks/dispatch_entry.ts --bundle --platform=node --format=esm --target=node20 --define:__AGENT_CONFIG_BUNDLE__=true --outfile=dist/hooks/dispatch.js --banner:js=\"import { createRequire as __acCreateRequire } from 'node:module'; const require = globalThis.require ?? __acCreateRequire(import.meta.url); const __acPath = require('node:path'); const __acFs = require('node:fs'); const __acUrl = require('node:url'); const __acReal = (p) => { try { return __acFs.realpathSync(p); } catch { return p; } }; const __acSelf = __acReal(__acUrl.fileURLToPath(import.meta.url)); if (process.argv[1] !== undefined && __acReal(__acPath.resolve(process.argv[1])) === __acSelf) { globalThis.__AC_HOOKS_BUNDLE_DIRECT = true; process.argv = [process.argv[0], __acSelf + '.__direct__', ...process.argv.slice(2)]; }\" && node src/scripts/write_bundle_digest.mjs",
|
|
84
84
|
"build:mcp-bundle": "esbuild src/scripts/mcp_server/__main__.ts --bundle --platform=node --format=esm --target=node20 --define:__AGENT_CONFIG_BUNDLE__=true --outfile=dist/mcp/server.mjs --banner:js=\"import { createRequire as __acCreateRequire } from 'node:module'; const require = globalThis.require ?? __acCreateRequire(import.meta.url); const __acPath = require('node:path'); const __acFs = require('node:fs'); const __acUrl = require('node:url'); const __acReal = (p) => { try { return __acFs.realpathSync(p); } catch { return p; } }; const __acSelf = __acReal(__acUrl.fileURLToPath(import.meta.url)); if (process.argv[1] !== undefined && __acReal(__acPath.resolve(process.argv[1])) === __acSelf) { globalThis.__AC_MCP_BUNDLE_DIRECT = true; process.argv = [process.argv[0], __acSelf + '.__direct__', ...process.argv.slice(2)]; }\"",
|
|
85
85
|
"clean:cli-delegate": "rm -rf dist/cli-delegate",
|
|
86
86
|
"build:delegates": "npm run clean:cli-delegate && npm run build:cli-delegate && npm run build:agent-src-delegate && npm run build:delegate-aux",
|
|
@@ -122,8 +122,8 @@
|
|
|
122
122
|
"tree-sitter-wasms": "0.1.13",
|
|
123
123
|
"tsx": "^4.23.1",
|
|
124
124
|
"typescript": "^5.9.3",
|
|
125
|
-
"vite": "^
|
|
126
|
-
"vitest": "^
|
|
125
|
+
"vite": "^8.3.1",
|
|
126
|
+
"vitest": "^5.0.2"
|
|
127
127
|
},
|
|
128
128
|
"dependencies": {
|
|
129
129
|
"@fastify/static": "^10.1.2",
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
*
|
|
12
12
|
* The gate is **scoped to the greenfield-scaffold path only**. It acts only
|
|
13
13
|
* when `state.ui_audit` records `greenfield == True` and
|
|
14
|
-
* `greenfield_decision
|
|
15
|
-
* clean `SUCCESS` no-op, so those flows stay
|
|
14
|
+
* `greenfield_decision` ∈ `{scaffold, granularity_convention}`. Every other
|
|
15
|
+
* UI flow sees this slot as a clean `SUCCESS` no-op, so those flows stay
|
|
16
|
+
* byte-identical.
|
|
16
17
|
*/
|
|
17
18
|
import {
|
|
18
19
|
type Any,
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
StepResult,
|
|
22
23
|
agent_directive,
|
|
23
24
|
} from '../../delivery_state.js';
|
|
25
|
+
import { GRANULARITY_CONVENTION } from '../../state.js';
|
|
24
26
|
|
|
25
27
|
/** Declared ambiguity surfaces for this step. */
|
|
26
28
|
export const AMBIGUITIES: ReadonlyArray<Record<string, string>> = [
|
|
@@ -112,14 +114,18 @@ export function run(state: DeliveryState): StepResult {
|
|
|
112
114
|
* The gate is inert for every other flow: improve-existing, the `bare` /
|
|
113
115
|
* `external_reference` greenfield picks, and the `diff` / `file` envelopes all
|
|
114
116
|
* leave `ui_audit` without the `greenfield == True` +
|
|
115
|
-
* `greenfield_decision
|
|
117
|
+
* `greenfield_decision` ∈ `{scaffold, granularity_convention}` pair.
|
|
116
118
|
*/
|
|
117
119
|
function _is_greenfield_scaffold(state: DeliveryState): boolean {
|
|
118
120
|
const audit = state.ui_audit;
|
|
119
121
|
if (!_isDict(audit)) return false;
|
|
122
|
+
// `granularity_convention` is option 4 — a scaffold that additionally
|
|
123
|
+
// organises what it creates under a named convention, so it takes the same
|
|
124
|
+
// grounding pass. Kept in step with the identical guard in `scaffold`.
|
|
120
125
|
return (
|
|
121
126
|
audit['greenfield'] === true &&
|
|
122
|
-
audit['greenfield_decision'] === 'scaffold'
|
|
127
|
+
(audit['greenfield_decision'] === 'scaffold' ||
|
|
128
|
+
audit['greenfield_decision'] === GRANULARITY_CONVENTION)
|
|
123
129
|
);
|
|
124
130
|
}
|
|
125
131
|
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
* rejection, or change recording. Apply validates the output against the
|
|
11
11
|
* brief's microcopy lock so a mid-loop hallucination is caught at the boundary.
|
|
12
12
|
*/
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
|
|
13
16
|
import {
|
|
14
17
|
type Any,
|
|
15
18
|
type DeliveryState,
|
|
@@ -17,6 +20,11 @@ import {
|
|
|
17
20
|
StepResult,
|
|
18
21
|
agent_directive,
|
|
19
22
|
} from '../../delivery_state.js';
|
|
23
|
+
import {
|
|
24
|
+
type ComponentRequest,
|
|
25
|
+
conformance_lines,
|
|
26
|
+
plan_component_placement,
|
|
27
|
+
} from '../../taxonomy/place.js';
|
|
20
28
|
import { has_design_system, placeholder_paths, provided_artifact } from './design.js';
|
|
21
29
|
import { _playbook_lines, _scaffold_playbooks } from './scaffold.js';
|
|
22
30
|
import {
|
|
@@ -118,15 +126,27 @@ export function run(state: DeliveryState): StepResult {
|
|
|
118
126
|
}
|
|
119
127
|
|
|
120
128
|
const provided = provided_artifact(state.ui_design as Record<string, Any> | null);
|
|
129
|
+
const notes: string[] = [];
|
|
121
130
|
if (provided !== null) {
|
|
122
|
-
const
|
|
123
|
-
if (gaps.length > 0) {
|
|
124
|
-
return _halt_coverage(state, provided, gaps);
|
|
131
|
+
const report = coverage_report(provided, envelope['coverage']);
|
|
132
|
+
if (report.gaps.length > 0) {
|
|
133
|
+
return _halt_coverage(state, provided, report.gaps);
|
|
134
|
+
}
|
|
135
|
+
notes.push(...report.fallbacks);
|
|
136
|
+
// 3.1 — reported in shadow: the outcome value is unchanged for one
|
|
137
|
+
// release, so a caller branching on it is found by the line rather
|
|
138
|
+
// than by the breakage.
|
|
139
|
+
const handed_back = _handed_back_line(report);
|
|
140
|
+
if (handed_back !== null) {
|
|
141
|
+
notes.push(handed_back);
|
|
125
142
|
}
|
|
126
143
|
}
|
|
127
144
|
|
|
128
145
|
_record_changes(state, envelope);
|
|
129
|
-
return new StepResult({
|
|
146
|
+
return new StepResult({
|
|
147
|
+
outcome: Outcome.SUCCESS,
|
|
148
|
+
message: notes.join('\n'),
|
|
149
|
+
});
|
|
130
150
|
}
|
|
131
151
|
|
|
132
152
|
/** Buckets the coverage report must sort every declared item into. */
|
|
@@ -139,8 +159,73 @@ export const COVERED_INVENTORIES: ReadonlyArray<string> = [
|
|
|
139
159
|
'assets',
|
|
140
160
|
];
|
|
141
161
|
|
|
162
|
+
/** What the coverage report said, split into what halts and what only warns. */
|
|
163
|
+
export interface CoverageReport {
|
|
164
|
+
/** Reasons the report fails to account for the artifact. A halt. */
|
|
165
|
+
gaps: string[];
|
|
166
|
+
/** Items carried only by the deprecated containment fallback. A warning. */
|
|
167
|
+
fallbacks: string[];
|
|
168
|
+
/** Every declared item, in report order. */
|
|
169
|
+
declared: string[];
|
|
170
|
+
/** Declared items whose only account is in `flagged` — work handed back. */
|
|
171
|
+
handed_back: string[];
|
|
172
|
+
}
|
|
173
|
+
|
|
142
174
|
/**
|
|
143
|
-
*
|
|
175
|
+
* Did the port hand back everything it was given?
|
|
176
|
+
*
|
|
177
|
+
* True only when there is a declared inventory and **every** item in it is
|
|
178
|
+
* accounted for solely by `flagged`. Deliberately narrow: flagging one dropped
|
|
179
|
+
* handler is the ledger working exactly as designed, and reporting that as a
|
|
180
|
+
* hand-back would make the signal worthless within a week. What this catches
|
|
181
|
+
* is the envelope that accounted for everything and carried none of it — which
|
|
182
|
+
* `coverage_gaps` cannot see, because a complete report and a complete
|
|
183
|
+
* surrender are the same empty gap list.
|
|
184
|
+
*/
|
|
185
|
+
export function carried_nothing(report: CoverageReport): boolean {
|
|
186
|
+
return report.declared.length > 0 && report.handed_back.length === report.declared.length;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The line that says a port handed its work back, or `null`. */
|
|
190
|
+
function _handed_back_line(report: CoverageReport): string | null {
|
|
191
|
+
if (!carried_nothing(report)) return null;
|
|
192
|
+
return (
|
|
193
|
+
`the port carried nothing over — all ${report.declared.length} declared ` +
|
|
194
|
+
`item(s) are in \`flagged\`: ${report.handed_back.join(', ')}`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Characters that make a containment hit part of a longer word. */
|
|
199
|
+
function _is_word_char(ch: string): boolean {
|
|
200
|
+
return (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Does `entry` mention `needle` as its own token rather than by accident?
|
|
205
|
+
*
|
|
206
|
+
* The deprecated fallback's boundary. Both arguments are already lower-cased.
|
|
207
|
+
* `submit handler — translated` mentions `submit handler`; `table sort order`
|
|
208
|
+
* does not mention `tab`, even though it contains those three letters. The
|
|
209
|
+
* discriminator is whether the hit is delimited on both sides, which is
|
|
210
|
+
* exactly the difference between an entry carrying its own explanation — the
|
|
211
|
+
* one thing containment was documented as buying — and a short name colliding
|
|
212
|
+
* with the inside of a longer word.
|
|
213
|
+
*/
|
|
214
|
+
function _mentions(entry: string, needle: string): boolean {
|
|
215
|
+
let from = 0;
|
|
216
|
+
for (;;) {
|
|
217
|
+
const at = entry.indexOf(needle, from);
|
|
218
|
+
if (at < 0) return false;
|
|
219
|
+
const before = at === 0 ? '' : (entry[at - 1] as string);
|
|
220
|
+
const after_at = at + needle.length;
|
|
221
|
+
const after = after_at >= entry.length ? '' : (entry[after_at] as string);
|
|
222
|
+
if (!_is_word_char(before) && !_is_word_char(after)) return true;
|
|
223
|
+
from = at + 1;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Return what the coverage report fails to account for, and what it only warns on.
|
|
144
229
|
*
|
|
145
230
|
* The fidelity ledger the port case never had. `apply` used to validate its
|
|
146
231
|
* output with a single placeholder substring scan and nothing else, so a
|
|
@@ -149,22 +234,45 @@ export const COVERED_INVENTORIES: ReadonlyArray<string> = [
|
|
|
149
234
|
* appear in exactly one bucket turns that silence into a halt: a handler the
|
|
150
235
|
* port could not carry has to be written down in `flagged`.
|
|
151
236
|
*
|
|
152
|
-
* Matching is
|
|
153
|
-
*
|
|
154
|
-
* action") and still
|
|
237
|
+
* **Matching is equality, not containment.** Containment was chosen so an
|
|
238
|
+
* entry could carry its own explanation ("submit handler — translated to a
|
|
239
|
+
* form action") and still account for `submit handler`. It also accounted for
|
|
240
|
+
* a declared `tab` with an entry about `table sort order`, which is the same
|
|
241
|
+
* rule doing the opposite of its job: the shorter the declared name, the more
|
|
242
|
+
* likely some unrelated entry contains it, so the gate grew weakest exactly
|
|
243
|
+
* where the inventory was tersest.
|
|
244
|
+
*
|
|
245
|
+
* `allow_annotated_fallback` keeps the explanation case working for one
|
|
246
|
+
* release, as a **warning** rather than a silent pass, so an envelope written
|
|
247
|
+
* against the old rule does not halt without notice. It is narrower than the
|
|
248
|
+
* containment it replaces: the mention has to be delimited
|
|
249
|
+
* ({@link _mentions}), which is the property the explanation case always had
|
|
250
|
+
* and the collision case never did.
|
|
155
251
|
*/
|
|
156
|
-
export function
|
|
252
|
+
export function coverage_report(
|
|
157
253
|
provided: Record<string, Any>,
|
|
158
254
|
coverage: Any,
|
|
159
|
-
|
|
255
|
+
allow_annotated_fallback = true,
|
|
256
|
+
): CoverageReport {
|
|
160
257
|
const gaps: string[] = [];
|
|
258
|
+
const fallbacks: string[] = [];
|
|
259
|
+
const declared_all: string[] = [];
|
|
260
|
+
const handed_back: string[] = [];
|
|
161
261
|
if (!_isDict(coverage)) {
|
|
162
|
-
return
|
|
163
|
-
|
|
164
|
-
'
|
|
165
|
-
|
|
262
|
+
return {
|
|
263
|
+
gaps: [
|
|
264
|
+
'no `coverage` report in the apply envelope — a provided artifact ' +
|
|
265
|
+
'requires one',
|
|
266
|
+
],
|
|
267
|
+
fallbacks,
|
|
268
|
+
declared: declared_all,
|
|
269
|
+
handed_back,
|
|
270
|
+
};
|
|
166
271
|
}
|
|
167
|
-
|
|
272
|
+
// Kept per-bucket, not flattened: which bucket accounted for an item is
|
|
273
|
+
// what separates a port that translated its work from one that handed it
|
|
274
|
+
// back, and a flat list of entries cannot answer that.
|
|
275
|
+
const by_bucket = new Map<string, string[]>();
|
|
168
276
|
for (const bucket of COVERAGE_BUCKETS) {
|
|
169
277
|
const value = coverage[bucket];
|
|
170
278
|
if (value === undefined) {
|
|
@@ -175,26 +283,75 @@ export function coverage_gaps(
|
|
|
175
283
|
gaps.push(`\`coverage.${bucket}\` must be a list of strings`);
|
|
176
284
|
continue;
|
|
177
285
|
}
|
|
286
|
+
const kept: string[] = [];
|
|
178
287
|
for (const item of value) {
|
|
179
288
|
if (typeof item === 'string' && item !== '') {
|
|
180
|
-
|
|
289
|
+
kept.push(item.toLowerCase());
|
|
181
290
|
}
|
|
182
291
|
}
|
|
292
|
+
by_bucket.set(bucket, kept);
|
|
293
|
+
}
|
|
294
|
+
/** The buckets that account for `needle`, by either rule. */
|
|
295
|
+
function _accounting_buckets(needle: string): string[] {
|
|
296
|
+
const hit: string[] = [];
|
|
297
|
+
for (const [bucket, entries] of by_bucket) {
|
|
298
|
+
if (entries.some((entry) => entry === needle)) {
|
|
299
|
+
hit.push(bucket);
|
|
300
|
+
} else if (
|
|
301
|
+
allow_annotated_fallback &&
|
|
302
|
+
entries.some((entry) => _mentions(entry, needle))
|
|
303
|
+
) {
|
|
304
|
+
hit.push(bucket);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return hit;
|
|
183
308
|
}
|
|
184
309
|
for (const inventory of COVERED_INVENTORIES) {
|
|
185
310
|
const declared = provided[inventory];
|
|
186
311
|
if (!Array.isArray(declared)) continue;
|
|
187
312
|
for (const item of declared) {
|
|
188
313
|
if (typeof item !== 'string' || item === '') continue;
|
|
314
|
+
declared_all.push(item);
|
|
189
315
|
const needle = item.toLowerCase();
|
|
190
|
-
|
|
316
|
+
const buckets = _accounting_buckets(needle);
|
|
317
|
+
if (buckets.length === 0) {
|
|
191
318
|
gaps.push(
|
|
192
319
|
`\`${inventory}\`: \`${item}\` appears in no coverage bucket`,
|
|
193
320
|
);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (buckets.length === 1 && buckets[0] === 'flagged') {
|
|
324
|
+
handed_back.push(item);
|
|
325
|
+
}
|
|
326
|
+
const exact = [...by_bucket.values()].some((entries) =>
|
|
327
|
+
entries.some((entry) => entry === needle),
|
|
328
|
+
);
|
|
329
|
+
if (exact) continue;
|
|
330
|
+
const mention = [...by_bucket.values()]
|
|
331
|
+
.flat()
|
|
332
|
+
.find((entry) => _mentions(entry, needle));
|
|
333
|
+
if (mention !== undefined) {
|
|
334
|
+
fallbacks.push(
|
|
335
|
+
`\`${inventory}\`: \`${item}\` matched only by containment ` +
|
|
336
|
+
`against \`${mention}\``,
|
|
337
|
+
);
|
|
194
338
|
}
|
|
195
339
|
}
|
|
196
340
|
}
|
|
197
|
-
return gaps;
|
|
341
|
+
return { gaps, fallbacks, declared: declared_all, handed_back };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Every reason the coverage report fails to account for the artifact.
|
|
346
|
+
*
|
|
347
|
+
* The halting half of {@link coverage_report}, kept as its own name because
|
|
348
|
+
* that is what callers and the port tests already ask for.
|
|
349
|
+
*/
|
|
350
|
+
export function coverage_gaps(
|
|
351
|
+
provided: Record<string, Any>,
|
|
352
|
+
coverage: Any,
|
|
353
|
+
): string[] {
|
|
354
|
+
return coverage_report(provided, coverage).gaps;
|
|
198
355
|
}
|
|
199
356
|
|
|
200
357
|
/** BLOCKED halt — the port did not account for what the artifact declared. */
|
|
@@ -243,6 +400,40 @@ function _apply_envelope(state: DeliveryState): Record<string, Any> | null {
|
|
|
243
400
|
return null;
|
|
244
401
|
}
|
|
245
402
|
|
|
403
|
+
/**
|
|
404
|
+
* Conformance lines for the taxonomy the audit detected, or nothing.
|
|
405
|
+
*
|
|
406
|
+
* Empty whenever `state.ui_audit.component_taxonomy` is absent or names no
|
|
407
|
+
* tiers — which is every project that has not chosen a granularity taxonomy,
|
|
408
|
+
* and is the case whose output must stay exactly as it was. Holding to the
|
|
409
|
+
* project's own structures is the standing rule; this step only ever conforms
|
|
410
|
+
* to a taxonomy the project itself evidences.
|
|
411
|
+
*
|
|
412
|
+
* The `'none'` case is NOT re-checked here. `conformance_lines` owns that
|
|
413
|
+
* invariant and says so in its own contract, and a second copy of the check
|
|
414
|
+
* here would mask a regression in the first: with both present, either one
|
|
415
|
+
* could be deleted and every AC-2 test would still pass. One owner, one test
|
|
416
|
+
* that fails when it breaks.
|
|
417
|
+
*/
|
|
418
|
+
export function taxonomy_lines(state: DeliveryState): string[] {
|
|
419
|
+
const audit = _isDict(state.ui_audit) ? (state.ui_audit as Record<string, Any>) : {};
|
|
420
|
+
const taxonomy = audit['component_taxonomy'];
|
|
421
|
+
if (typeof taxonomy !== 'string' || taxonomy === '') {
|
|
422
|
+
return [];
|
|
423
|
+
}
|
|
424
|
+
const root = typeof audit['component_root'] === 'string' ? audit['component_root'] : '';
|
|
425
|
+
const design = _isDict(state.ui_design) ? (state.ui_design as Record<string, Any>) : {};
|
|
426
|
+
const raw = design['components'];
|
|
427
|
+
const components: ComponentRequest[] = Array.isArray(raw)
|
|
428
|
+
? raw.filter((c): c is Record<string, Any> => _isDict(c))
|
|
429
|
+
: [];
|
|
430
|
+
return conformance_lines(
|
|
431
|
+
taxonomy,
|
|
432
|
+
root,
|
|
433
|
+
plan_component_placement(taxonomy, root, components),
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
246
437
|
/** Pick the agent directive for the project's frontend stack. */
|
|
247
438
|
function _resolve_directive(state: DeliveryState): string {
|
|
248
439
|
const stack = _pyTruthy(state.stack) ? state.stack : {};
|
|
@@ -265,7 +456,53 @@ function _resolve_directive(state: DeliveryState): string {
|
|
|
265
456
|
* exactly the same inputs.
|
|
266
457
|
*/
|
|
267
458
|
function _placeholder_violations_in_output(envelope: Record<string, Any>): string[] {
|
|
268
|
-
return placeholder_paths(envelope['rendered']);
|
|
459
|
+
return [...placeholder_paths(envelope['rendered']), ...written_file_placeholders(envelope)];
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Return the written files whose text carries a placeholder pattern.
|
|
464
|
+
*
|
|
465
|
+
* `rendered` is the porter's own report of what it wrote. The files are what
|
|
466
|
+
* it actually wrote, and nothing made the two agree — a placeholder that never
|
|
467
|
+
* entered the report was invisible to a gate that only read the report. This
|
|
468
|
+
* reads the other side of that pair.
|
|
469
|
+
*
|
|
470
|
+
* **Only `envelope['files']`** — the set the port declares it changed. Never a
|
|
471
|
+
* tree sweep: the directive has no business reading files this run did not
|
|
472
|
+
* touch, and a sweep would grow with the repository rather than with the port.
|
|
473
|
+
*
|
|
474
|
+
* A path that does not resolve to a readable file is **skipped, not reported**.
|
|
475
|
+
* `apply` runs at points where a declared file may not be on disk yet, and
|
|
476
|
+
* halting a correct port because a path failed to resolve would be a worse
|
|
477
|
+
* failure than the one this closes. The cost is real and worth naming: a
|
|
478
|
+
* placeholder inside a file the engine cannot read stays unseen.
|
|
479
|
+
*
|
|
480
|
+
* `root` defaults to the working directory, which is the consumer project root
|
|
481
|
+
* when the engine runs — the same convention as `scaffold`'s token lookup.
|
|
482
|
+
*/
|
|
483
|
+
export function written_file_placeholders(
|
|
484
|
+
envelope: Record<string, Any>,
|
|
485
|
+
root: string | null = null,
|
|
486
|
+
): string[] {
|
|
487
|
+
const files = envelope['files'];
|
|
488
|
+
if (!Array.isArray(files)) return [];
|
|
489
|
+
const base = root !== null ? root : process.cwd();
|
|
490
|
+
const hits: string[] = [];
|
|
491
|
+
for (const rel of files as Any[]) {
|
|
492
|
+
if (typeof rel !== 'string' || rel === '') continue;
|
|
493
|
+
let text: string;
|
|
494
|
+
try {
|
|
495
|
+
const full = path.resolve(base, rel);
|
|
496
|
+
if (!fs.statSync(full).isFile()) continue;
|
|
497
|
+
text = fs.readFileSync(full, 'utf8');
|
|
498
|
+
} catch {
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
if (placeholder_paths(text, rel).length > 0) {
|
|
502
|
+
hits.push(rel);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return hits;
|
|
269
506
|
}
|
|
270
507
|
|
|
271
508
|
/** First-pass halt — emit the stack-specific apply directive. */
|
|
@@ -298,6 +535,7 @@ function _delegate_to_stack_skill(state: DeliveryState): StepResult {
|
|
|
298
535
|
'> Microcopy is locked — every button label, empty-state ' +
|
|
299
536
|
'message, and validation message must come verbatim from ' +
|
|
300
537
|
'`state.ui_design.microcopy`.',
|
|
538
|
+
...taxonomy_lines(state),
|
|
301
539
|
];
|
|
302
540
|
if (provided !== null) {
|
|
303
541
|
lines.push(
|
|
@@ -331,9 +569,10 @@ function _halt_placeholders(state: DeliveryState, violations: string[]): StepRes
|
|
|
331
569
|
const directive = _resolve_directive(state);
|
|
332
570
|
const lines: string[] = [
|
|
333
571
|
agent_directive(directive),
|
|
334
|
-
'> Apply rejected:
|
|
572
|
+
'> Apply rejected: the port output contains placeholder strings. ' +
|
|
335
573
|
'The design-brief microcopy lock failed mid-loop.',
|
|
336
|
-
'> Affected paths in `ui_apply.rendered
|
|
574
|
+
'> Affected paths, in `ui_apply.rendered` and in the files the ' +
|
|
575
|
+
'envelope declares it wrote:',
|
|
337
576
|
];
|
|
338
577
|
for (const p of violations) {
|
|
339
578
|
lines.push(`> - \`${p}\``);
|
|
@@ -19,6 +19,20 @@ import {
|
|
|
19
19
|
agent_directive,
|
|
20
20
|
} from '../../delivery_state.js';
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* The greenfield pick that adopts a component-granularity convention.
|
|
24
|
+
*
|
|
25
|
+
* Re-exported from the schema, which owns it beside `GREENFIELD_DECISIONS` —
|
|
26
|
+
* an option the user can pick that the schema then rejects is a halt with a
|
|
27
|
+
* dead exit, so the two cannot be allowed to drift.
|
|
28
|
+
*
|
|
29
|
+
* Offered only here, and only as an OFFER: a project that already exists is
|
|
30
|
+
* never asked, and a user who picks any other number has declined it for good
|
|
31
|
+
* — the halt is a no-op once a decision is recorded, so declining costs one
|
|
32
|
+
* number and is never raised again.
|
|
33
|
+
*/
|
|
34
|
+
export { GRANULARITY_CONVENTION } from '../../state.js';
|
|
35
|
+
|
|
22
36
|
/** Similarity threshold for a "strong reusable match". */
|
|
23
37
|
export const STRONG_SIMILARITY = 0.7;
|
|
24
38
|
|
|
@@ -42,8 +56,9 @@ export const AMBIGUITIES: ReadonlyArray<Record<string, string>> = [
|
|
|
42
56
|
'state.ui_audit.greenfield is True but greenfield_decision ' +
|
|
43
57
|
'is unset — user has not picked a scaffolding direction',
|
|
44
58
|
resolution:
|
|
45
|
-
'user picks scaffold / bare / external_reference
|
|
46
|
-
'agent records the choice in
|
|
59
|
+
'user picks scaffold / bare / external_reference / ' +
|
|
60
|
+
'granularity_convention; agent records the choice in ' +
|
|
61
|
+
'state.ui_audit.greenfield_decision',
|
|
47
62
|
},
|
|
48
63
|
{
|
|
49
64
|
code: 'shadcn_version_mismatch',
|
|
@@ -198,6 +213,9 @@ function _halt_greenfield(state: DeliveryState, audit: Record<string, Any>): Ste
|
|
|
198
213
|
'> 1. Scaffold — minimal token set + base component primitive folder',
|
|
199
214
|
'> 2. Bare — proceed with Tailwind defaults, no scaffolding',
|
|
200
215
|
'> 3. External reference — point me at a design-system URL or file',
|
|
216
|
+
'> 4. Scaffold + a granularity convention — as 1, and new components ' +
|
|
217
|
+
'go into named granularity folders you pick the names for; ' +
|
|
218
|
+
'rename or drop it later and nothing here objects',
|
|
201
219
|
'',
|
|
202
220
|
'**Recommendation: 1 — Scaffold tokens + primitives** ' +
|
|
203
221
|
'— even one extra screen benefits from a shared base; the ' +
|
|
@@ -210,7 +228,8 @@ function _halt_greenfield(state: DeliveryState, audit: Record<string, Any>): Ste
|
|
|
210
228
|
questions,
|
|
211
229
|
message:
|
|
212
230
|
'UI audit detected greenfield; halting for scaffolding ' +
|
|
213
|
-
'direction (scaffold / bare / external_reference
|
|
231
|
+
'direction (scaffold / bare / external_reference / ' +
|
|
232
|
+
'granularity_convention).',
|
|
214
233
|
});
|
|
215
234
|
}
|
|
216
235
|
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
StepResult,
|
|
43
43
|
agent_directive,
|
|
44
44
|
} from '../../delivery_state.js';
|
|
45
|
+
import { GRANULARITY_CONVENTION } from '../../state.js';
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
48
|
* Conventional locations for a project's DTCG `tokens.json`.
|
|
@@ -307,9 +308,13 @@ export function run(state: DeliveryState): StepResult {
|
|
|
307
308
|
function _is_greenfield_scaffold(state: DeliveryState): boolean {
|
|
308
309
|
const audit = state.ui_audit;
|
|
309
310
|
if (!_isDict(audit)) return false;
|
|
311
|
+
// `granularity_convention` is option 4 — a scaffold that additionally
|
|
312
|
+
// organises what it creates under a named convention. It has to reach the
|
|
313
|
+
// scaffold gate, or picking 4 would scaffold nothing at all.
|
|
310
314
|
return (
|
|
311
315
|
audit['greenfield'] === true &&
|
|
312
|
-
audit['greenfield_decision'] === 'scaffold'
|
|
316
|
+
(audit['greenfield_decision'] === 'scaffold' ||
|
|
317
|
+
audit['greenfield_decision'] === GRANULARITY_CONVENTION)
|
|
313
318
|
);
|
|
314
319
|
}
|
|
315
320
|
|
|
@@ -27,7 +27,10 @@
|
|
|
27
27
|
* `existing-ui-audit` skill (R3 Phase 2). `null` while the audit
|
|
28
28
|
* has not run; populated dict once the skill returns. `greenfield`
|
|
29
29
|
* flag plus `greenfield_decision` carry the user's scaffolding
|
|
30
|
-
* pick
|
|
30
|
+
* pick; `component_taxonomy` and `component_root` carry the
|
|
31
|
+
* granularity taxonomy the project itself evidences, or `'none'`.
|
|
32
|
+
* `'none'` is the value that leaves every downstream step behaving
|
|
33
|
+
* exactly as it did before detection existed. The audit gate (`work_engine.directives.ui.audit`)
|
|
31
34
|
* refuses to advance to design/apply while the slot is empty or
|
|
32
35
|
* while `greenfield` is set without a recorded decision.
|
|
33
36
|
* - `app_spec` — optional greenfield grounding artifact written by
|
|
@@ -40,7 +43,12 @@
|
|
|
40
43
|
* - `ui_design` — optional design brief produced by
|
|
41
44
|
* `work_engine.directives.ui.design` (R3 Phase 3 Step 1). Locks
|
|
42
45
|
* layout / components / states / microcopy / a11y; `design_confirmed`
|
|
43
|
-
* carries the user's sign-off.
|
|
46
|
+
* carries the user's sign-off. A `components[]` entry MAY carry a
|
|
47
|
+
* `tier` naming one of `ui_audit.component_taxonomy`'s tiers; the
|
|
48
|
+
* apply step reads it to name the target directory, and it is the
|
|
49
|
+
* only producer of that field. Omit it and the component is
|
|
50
|
+
* reported as a conformance gap rather than placed, which is the
|
|
51
|
+
* honest answer when nothing said where it belongs.
|
|
44
52
|
* - `ui_scaffold` — optional greenfield scaffold plan written by
|
|
45
53
|
* `work_engine.directives.ui.scaffold` (greenfield-scaffold
|
|
46
54
|
* Phase 3). Plan-only and stack-agnostic
|
|
@@ -172,6 +180,37 @@ export const KNOWN_DIRECTIVE_SETS: ReadonlySet<string> = new Set([
|
|
|
172
180
|
'mixed',
|
|
173
181
|
]);
|
|
174
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Every value `state.ui_audit.greenfield_decision` may carry.
|
|
185
|
+
*
|
|
186
|
+
* One list, read by the schema here and by the halt that offers the options
|
|
187
|
+
* (`directives/ui/audit.ts`), so an option the user can pick and a value the
|
|
188
|
+
* schema accepts cannot drift apart.
|
|
189
|
+
*
|
|
190
|
+
* `granularity_convention` is option 4: scaffold, and organise what is
|
|
191
|
+
* scaffolded under a named granularity convention. It is a fourth value of the
|
|
192
|
+
* same single-number question rather than a second axis — one number still
|
|
193
|
+
* answers the block, which is what the reply-shape rule requires.
|
|
194
|
+
*/
|
|
195
|
+
export const GREENFIELD_DECISIONS: ReadonlyArray<string> = [
|
|
196
|
+
'scaffold',
|
|
197
|
+
'bare',
|
|
198
|
+
'external_reference',
|
|
199
|
+
'granularity_convention',
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The greenfield pick that adopts a component-granularity convention.
|
|
204
|
+
*
|
|
205
|
+
* Exported HERE, beside the list it belongs to, and imported by every gate
|
|
206
|
+
* that tests for it — the audit halt that offers it, and the scaffold and
|
|
207
|
+
* app-spec gates that must let it through. It was a bare string literal in
|
|
208
|
+
* four places; a rename would have left those gates silently not matching,
|
|
209
|
+
* and an inert scaffold gate returns SUCCESS, so picking option 4 would have
|
|
210
|
+
* scaffolded nothing with no error anywhere.
|
|
211
|
+
*/
|
|
212
|
+
export const GRANULARITY_CONVENTION = 'granularity_convention';
|
|
213
|
+
|
|
175
214
|
/** Raised when a state payload violates the v1 contract. */
|
|
176
215
|
export class SchemaError extends Error {
|
|
177
216
|
constructor(message: string) {
|
|
@@ -535,7 +574,7 @@ function _validate_stack(stack: JsonValue): void {
|
|
|
535
574
|
* skill returns but before findings land; the gate treats it the
|
|
536
575
|
* same as `null`. Once populated, `greenfield` (when present)
|
|
537
576
|
* must be a bool, and `greenfield_decision` (when present) must
|
|
538
|
-
* be one of
|
|
577
|
+
* be one of `GREENFIELD_DECISIONS`. Other keys (`components`,
|
|
539
578
|
* `patterns`, ...) are validated by the audit handler against the
|
|
540
579
|
* skill contract — the schema only enforces shape, not content.
|
|
541
580
|
*/
|
|
@@ -559,16 +598,27 @@ function _validate_ui_audit(ui_audit: JsonValue): void {
|
|
|
559
598
|
: null;
|
|
560
599
|
if (
|
|
561
600
|
decision !== null &&
|
|
562
|
-
decision
|
|
563
|
-
decision !== 'bare' &&
|
|
564
|
-
decision !== 'external_reference'
|
|
601
|
+
!(typeof decision === 'string' && GREENFIELD_DECISIONS.includes(decision))
|
|
565
602
|
) {
|
|
566
603
|
throw new SchemaError(
|
|
567
604
|
`state.ui_audit.greenfield_decision must be one of ` +
|
|
568
|
-
`'
|
|
605
|
+
`${GREENFIELD_DECISIONS.map((d) => `'${d}'`).join(', ')}, or null; ` +
|
|
569
606
|
`got ${pyRepr(decision)}`,
|
|
570
607
|
);
|
|
571
608
|
}
|
|
609
|
+
const taxonomy = 'component_taxonomy' in ui_audit ? ui_audit['component_taxonomy'] : null;
|
|
610
|
+
if (taxonomy !== null && typeof taxonomy !== 'string') {
|
|
611
|
+
throw new SchemaError(
|
|
612
|
+
'state.ui_audit.component_taxonomy must be a string when present ' +
|
|
613
|
+
"— the project's own tiers joined with '/', or 'none'",
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
const root = 'component_root' in ui_audit ? ui_audit['component_root'] : null;
|
|
617
|
+
if (root !== null && typeof root !== 'string') {
|
|
618
|
+
throw new SchemaError(
|
|
619
|
+
'state.ui_audit.component_root must be a string when present',
|
|
620
|
+
);
|
|
621
|
+
}
|
|
572
622
|
if (
|
|
573
623
|
'a11y_baseline' in ui_audit &&
|
|
574
624
|
!Array.isArray(ui_audit['a11y_baseline'])
|