@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.
Files changed (217) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +237 -248
  5. package/dist/agent-src/commands/roadmap/process-full.md +1 -1
  6. package/dist/agent-src/commands/work.md +4 -0
  7. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +4 -8
  8. package/dist/agent-src/rules/ui-audit-gate.md +1 -2
  9. package/dist/agent-src/skills/accessibility-auditor/data/aria-patterns.csv +1 -1
  10. package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +2 -2
  11. package/dist/agent-src/skills/design-review/SKILL.md +30 -4
  12. package/dist/agent-src/skills/design-review/references/verification-automation.md +32 -0
  13. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +16 -1
  14. package/dist/agent-src/skills/existing-ui-audit/references/component-taxonomy-detection.md +56 -0
  15. package/dist/agent-src/skills/markitdown/SKILL.md +4 -0
  16. package/dist/agent-src/skills/playwright-testing/SKILL.md +30 -6
  17. package/dist/agent-src/skills/ui-component-architect/SKILL.md +23 -0
  18. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  19. package/dist/agent-src/templates/roadmaps.md +30 -0
  20. package/dist/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
  21. package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
  22. package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
  23. package/dist/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
  24. package/dist/agent-src/templates/scripts/work_engine/state.ts +57 -7
  25. package/dist/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
  26. package/dist/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
  27. package/dist/cli/python/knowledge_ingest.js +1 -1
  28. package/dist/cli/python/workspace_drive.js +1 -1
  29. package/dist/cli-delegate/{chunk-S3VFXMTM.js → chunk-4XFP2Y3E.js} +1 -1
  30. package/dist/cli-delegate/{chunk-HDVVGPQW.js → chunk-7XGBX6ZH.js} +471 -387
  31. package/dist/cli-delegate/{chunk-GJCBB2UG.js → chunk-FP36SWJD.js} +432 -144
  32. package/dist/cli-delegate/{chunk-UNCRJBT3.js → chunk-IGD6ERX6.js} +31 -0
  33. package/dist/cli-delegate/{chunk-FXYUYU4N.js → chunk-JNMVLWLR.js} +42 -1
  34. package/dist/cli-delegate/{chunk-QJWAUMJW.js → chunk-YJ72UIAK.js} +730 -15
  35. package/dist/cli-delegate/cmd_conformance.js +902 -782
  36. package/dist/cli-delegate/cmd_doctor.js +2 -2
  37. package/dist/cli-delegate/cmd_export.js +2 -2
  38. package/dist/cli-delegate/cmd_handoff.js +4 -4
  39. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  40. package/dist/cli-delegate/cmd_prune.js +2 -2
  41. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  42. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  43. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  44. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  45. package/dist/cli-delegate/cmd_sync.js +2 -2
  46. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  47. package/dist/cli-delegate/cmd_validate.js +2 -2
  48. package/dist/discovery/deprecation-report.md +1 -1
  49. package/dist/discovery/discovery-manifest.json +10 -10
  50. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  51. package/dist/discovery/discovery-manifest.summary.md +1 -1
  52. package/dist/discovery/dormancy-report.md +1 -1
  53. package/dist/discovery/orphan-report.md +1 -1
  54. package/dist/discovery/packs.json +2 -2
  55. package/dist/discovery/trust-report.md +1 -1
  56. package/dist/discovery/workspaces.json +2 -2
  57. package/dist/hooks/dispatch.js +6666 -2581
  58. package/dist/hooks/dispatch.sha256 +1 -0
  59. package/dist/install/install.mjs +848 -733
  60. package/dist/mcp/registry-manifest.json +2 -2
  61. package/dist/mcp/server.json +2 -2
  62. package/dist/mcp/server.mjs +2747 -1510
  63. package/dist/server/schemas/settings.js +23 -0
  64. package/dist/ui/assets/index-Cwq4d0hX.js +23 -0
  65. package/dist/ui/assets/index-Cyjy8bWO.css +1 -0
  66. package/dist/ui/index.html +2 -2
  67. package/docs/contracts/settings-classes.md +6 -4
  68. package/package.json +4 -4
  69. package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
  70. package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
  71. package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
  72. package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
  73. package/src/agent-src/templates/scripts/work_engine/state.ts +57 -7
  74. package/src/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
  75. package/src/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
  76. package/src/config/agent-settings.template.yml +29 -0
  77. package/src/config/ci-local-parity.yml +18 -0
  78. package/src/config/continuity-surface.json +2 -2
  79. package/src/config/gate-coverage.yml +234 -3
  80. package/src/config/gate-violation-baselines.json +7 -3
  81. package/src/config/hook-latency-budget.json +21 -0
  82. package/src/config/hook-token-budget.json +2 -2
  83. package/src/config/metric-registry.yml +114 -0
  84. package/src/config/placeholder-drift-budget.json +10 -0
  85. package/src/config/routing-coverage-seed.json +18 -4
  86. package/src/scripts/_cli/cmd_doctor.ts +5 -19
  87. package/src/scripts/_cli/doctor_execution.ts +100 -17
  88. package/src/scripts/_cli/doctor_network_posture.ts +226 -0
  89. package/src/scripts/_lib/body_portable.ts +102 -0
  90. package/src/scripts/_lib/cc_transcript.ts +132 -1
  91. package/src/scripts/_lib/concern_sla_window.ts +200 -0
  92. package/src/scripts/_lib/forge_protection.ts +114 -0
  93. package/src/scripts/_lib/forge_reader.ts +558 -0
  94. package/src/scripts/_lib/host_env_write.ts +111 -0
  95. package/src/scripts/_lib/llm_proposer_transport.ts +14 -4
  96. package/src/scripts/_lib/obligations.ts +119 -8
  97. package/src/scripts/_lib/probe_inputs.ts +271 -0
  98. package/src/scripts/_lib/read_surface_scan.ts +322 -0
  99. package/src/scripts/_lib/release_highlights.ts +2 -1
  100. package/src/scripts/_lib/retrieval_sanitize.ts +77 -16
  101. package/src/scripts/_lib/shell_write_shapes.ts +108 -0
  102. package/src/scripts/_lib/structural_hiding.ts +622 -0
  103. package/src/scripts/_lib/turn_end_refusals.ts +525 -4
  104. package/src/scripts/_lib/turn_end_transcript.ts +216 -0
  105. package/src/scripts/_lib/verification_command.ts +324 -0
  106. package/src/scripts/_lib/verification_evidence.ts +647 -0
  107. package/src/scripts/_lib/verify_clause.ts +280 -0
  108. package/src/scripts/ai_team/team_dispatch.ts +4 -0
  109. package/src/scripts/before_complete_hook.ts +340 -23
  110. package/src/scripts/bench_hook_latency.ts +412 -5
  111. package/src/scripts/cache_realization_report.ts +61 -1
  112. package/src/scripts/check_argument_hint.ts +287 -0
  113. package/src/scripts/check_claims.ts +125 -1
  114. package/src/scripts/check_enforcement_coverage.ts +38 -9
  115. package/src/scripts/check_enforcement_matrix.ts +245 -3
  116. package/src/scripts/check_finding_dispositions.ts +22 -3
  117. package/src/scripts/check_generator_sync.ts +71 -0
  118. package/src/scripts/check_host_docs_digest.ts +786 -0
  119. package/src/scripts/check_host_format_column.ts +559 -0
  120. package/src/scripts/check_input_references.ts +228 -0
  121. package/src/scripts/check_inputs_parity.ts +235 -0
  122. package/src/scripts/check_kernel_edit_ratified.ts +47 -5
  123. package/src/scripts/check_kill_switch_table.ts +294 -0
  124. package/src/scripts/check_placeholder_drift.ts +221 -0
  125. package/src/scripts/check_read_surface_coverage.ts +515 -0
  126. package/src/scripts/check_release_holds.ts +2 -1
  127. package/src/scripts/check_routing_coverage.ts +378 -11
  128. package/src/scripts/check_static_parity.ts +17 -0
  129. package/src/scripts/ci_settle.ts +145 -19
  130. package/src/scripts/closure_scan.ts +115 -1
  131. package/src/scripts/compile_hook_manifest.ts +60 -16
  132. package/src/scripts/external_sources_denylist.json +29 -4
  133. package/src/scripts/hook_manifest.json +1 -1
  134. package/src/scripts/hook_manifest.yaml +42 -5
  135. package/src/scripts/hooks/block_config_weakening.ts +229 -4
  136. package/src/scripts/hooks/block_kernel_rule_writes.ts +9 -78
  137. package/src/scripts/hooks/block_plumbing_writes.ts +314 -0
  138. package/src/scripts/hooks/bundle_integrity.ts +291 -0
  139. package/src/scripts/hooks/chain_nudge_hook.ts +1 -2
  140. package/src/scripts/hooks/code_graph_context_hook.ts +1 -2
  141. package/src/scripts/hooks/comment_discipline_hook.ts +1 -3
  142. package/src/scripts/hooks/concern_registry.ts +2 -0
  143. package/src/scripts/hooks/concern_timings.ts +94 -0
  144. package/src/scripts/hooks/delegation_nudge_hook.ts +1 -2
  145. package/src/scripts/hooks/design_pass_hook.ts +100 -9
  146. package/src/scripts/hooks/design_slop_hook.ts +1 -2
  147. package/src/scripts/hooks/dispatch_hook.ts +25 -19
  148. package/src/scripts/hooks/edit_shape_hook.ts +1 -2
  149. package/src/scripts/hooks/end_review_nudge_hook.ts +1 -1
  150. package/src/scripts/hooks/evidence_independence.ts +1 -2
  151. package/src/scripts/hooks/exit_codes.ts +128 -0
  152. package/src/scripts/hooks/host_lowering.json +1 -0
  153. package/src/scripts/hooks/host_lowering.ts +128 -3
  154. package/src/scripts/hooks/host_lowering.yaml +262 -46
  155. package/src/scripts/hooks/host_semantics.ts +30 -2
  156. package/src/scripts/hooks/injection_budget.ts +9 -2
  157. package/src/scripts/hooks/interruption_ledger_hook.ts +1 -2
  158. package/src/scripts/hooks/journal_record_hook.ts +1 -2
  159. package/src/scripts/hooks/memory_recall_hook.ts +1 -2
  160. package/src/scripts/hooks/obligation_settle_hook.ts +151 -5
  161. package/src/scripts/hooks/one_question_per_ask_hook.ts +25 -4
  162. package/src/scripts/hooks/orchestration_record_hook.ts +1 -2
  163. package/src/scripts/hooks/permission_gate.ts +1 -2
  164. package/src/scripts/hooks/push_settle_hook.ts +1 -3
  165. package/src/scripts/hooks/replay_hook.ts +2 -1
  166. package/src/scripts/hooks/reread_guard_hook.ts +1 -2
  167. package/src/scripts/hooks/review_baseline_hook.ts +1 -2
  168. package/src/scripts/hooks/rtk_wrap_hook.ts +1 -2
  169. package/src/scripts/hooks/rule_inject_hook.ts +8 -5
  170. package/src/scripts/hooks/run_continuation_hook.ts +13 -15
  171. package/src/scripts/hooks/skill_route_hook.ts +1 -2
  172. package/src/scripts/hooks/source_first_gate_hook.ts +1 -2
  173. package/src/scripts/hooks/spawn_guard_shadow_hook.ts +1 -2
  174. package/src/scripts/hooks/subagent_ledger_hook.ts +1 -2
  175. package/src/scripts/hooks/suggestion_capture_hook.ts +1 -1
  176. package/src/scripts/hooks/table_fingerprint.ts +49 -0
  177. package/src/scripts/hooks/telemetry_flush_hook.ts +1 -2
  178. package/src/scripts/hooks/telemetry_usage_hook.ts +1 -2
  179. package/src/scripts/hooks/tool_result_bytes_hook.ts +93 -9
  180. package/src/scripts/hooks/turn_end_gate_hook.ts +337 -324
  181. package/src/scripts/hooks/ui_route_nudge_hook.ts +1 -2
  182. package/src/scripts/install.ts +3 -4
  183. package/src/scripts/lint_deny_text.ts +419 -0
  184. package/src/scripts/lint_exit_codes.ts +262 -0
  185. package/src/scripts/lint_hook_manifest.ts +55 -0
  186. package/src/scripts/lint_roadmap_blockers.ts +78 -12
  187. package/src/scripts/lint_skill_descriptions.ts +1 -1
  188. package/src/scripts/lint_skill_scripts_readonly.ts +1 -1
  189. package/src/scripts/mcp_server/prompts.ts +142 -1
  190. package/src/scripts/measure_skill_ranker_baseline.ts +305 -51
  191. package/src/scripts/measure_turn_end_gate.ts +193 -0
  192. package/src/scripts/memory_lookup.ts +1 -1
  193. package/src/scripts/prepack-check.mjs +53 -0
  194. package/src/scripts/release_publication.ts +13 -1
  195. package/src/scripts/report_invocation_surface.ts +383 -0
  196. package/src/scripts/report_obligation_carriers.ts +699 -0
  197. package/src/scripts/report_obligation_writer_split.ts +236 -0
  198. package/src/scripts/report_skill_menu_census.ts +208 -0
  199. package/src/scripts/report_standing_payload_by_host.ts +1 -1
  200. package/src/scripts/roadmap_verify_share.ts +213 -0
  201. package/src/scripts/schemas/command.schema.json +44 -0
  202. package/src/scripts/schemas/review-findings.schema.json +4 -0
  203. package/src/scripts/schemas/skill.schema.json +44 -0
  204. package/src/scripts/self_review_gate.ts +359 -10
  205. package/src/scripts/semantic_parity_census.ts +314 -0
  206. package/src/scripts/session_eol_report.ts +54 -1
  207. package/src/scripts/sweep_dead_scan_roots.ts +1 -1
  208. package/src/scripts/trigger_eval_grandfather.json +0 -2
  209. package/src/scripts/trigger_eval_rotation.ts +122 -27
  210. package/src/scripts/ui_conformance_probe.ts +156 -4
  211. package/src/scripts/update_prices.ts +85 -3
  212. package/src/scripts/write_bundle_digest.mjs +74 -0
  213. package/src/server/schemas/settings.ts +31 -0
  214. package/src/templates/consumer-settings/ONBOARDING.md +1 -0
  215. package/dist/ui/assets/index-DH1xc62v.js +0 -29
  216. package/dist/ui/assets/index-jSfMW8_t.css +0 -1
  217. /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.1.0",
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": "^5.4.21",
126
- "vitest": "^2.1.9"
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 == "scaffold"`. Every other UI flow sees this slot as a
15
- * clean `SUCCESS` no-op, so those flows stay byte-identical.
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 == "scaffold"` pair.
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 gaps = coverage_gaps(provided, envelope['coverage']);
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({ outcome: Outcome.SUCCESS });
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
- * Return every reason the coverage report fails to account for the artifact.
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 substring containment against the bucket entries, so an entry
153
- * may carry its own explanation ("submit handler — translated to a form
154
- * action") and still count as accounting for `submit handler`.
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 coverage_gaps(
252
+ export function coverage_report(
157
253
  provided: Record<string, Any>,
158
254
  coverage: Any,
159
- ): string[] {
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
- 'no `coverage` report in the apply envelope — a provided artifact ' +
164
- 'requires one',
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
- const entries: string[] = [];
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
- entries.push(item.toLowerCase());
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
- if (!entries.some((entry) => entry.includes(needle))) {
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: rendered output contains placeholder strings. ' +
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 state.ui_audit.greenfield_decision',
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. The audit gate (`work_engine.directives.ui.audit`)
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 the three documented choices. Other keys (`components`,
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 !== 'scaffold' &&
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
- `'scaffold', 'bare', 'external_reference', or null; ` +
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'])