@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
@@ -0,0 +1,500 @@
1
+ /**
2
+ * Component-granularity taxonomy detection from a project's own layout.
3
+ *
4
+ * Leaf module — stdlib only, NO intra-`work_engine` imports — so it can be
5
+ * called from a directive, a skill runner, or a test without pulling the
6
+ * engine in. Public API names stay snake_case to mirror the sibling detectors
7
+ * (`stack/detect.ts`, `stack/runner.ts`) 1:1, per ADR-200.
8
+ *
9
+ * What this answers, and what it deliberately does not:
10
+ *
11
+ * The question is *"has this project already chosen a way to organise its
12
+ * components by granularity, and if so what does it call the tiers?"* — never
13
+ * *"which taxonomy should this project use?"*. The recorded answer is the
14
+ * project's OWN tier directory names in the project's own order, joined with
15
+ * `/`, or {@link NO_TAXONOMY}. Nothing canonical is ever written into the
16
+ * record: a project with three tiers records three tiers, and a project whose
17
+ * tiers are called `primitives / patterns / features` records exactly that.
18
+ *
19
+ * That is the whole point of the module. The suite follows a taxonomy a
20
+ * project has chosen; it never forces one onto a project that has not.
21
+ *
22
+ * Why there is a lexicon at all, and what bounds it:
23
+ *
24
+ * Deciding *"is `components/checkout/` a granularity tier or a domain
25
+ * folder?"* cannot be done from structure alone — both are directories full of
26
+ * components. So {@link GRANULARITY_LEXICON} exists as a **recognition aid**:
27
+ * an unordered set of granularity words drawn from several vocabularies, with
28
+ * no ordering, no completeness claim, no level semantics, and no per-tier
29
+ * rules of any kind. It is read to answer "does this bucket name look like a
30
+ * granularity word", and for nothing else.
31
+ *
32
+ * In particular it is NEVER read when a component is placed: placement reads
33
+ * the recorded tier list by name (see `./place.ts`). A project whose tiers are
34
+ * in no lexicon still gets conformance, because a declaration in its own docs
35
+ * outranks inference — which is the path the `declared` fixture exercises.
36
+ *
37
+ * Evidence rules:
38
+ *
39
+ * 1. **Declared beats inferred.** A `## Component taxonomy` section in the
40
+ * project's `DESIGN.md` naming backticked directory names wins outright,
41
+ * provided at least {@link MIN_TIERS} of those names exist on disk under
42
+ * the component root. Grounding the declaration in the filesystem keeps a
43
+ * stale doc from declaring a taxonomy the tree does not have.
44
+ * 2. **Inference needs a majority, never a single hit.** At least
45
+ * {@link MIN_TIERS} immediate buckets under the component root must carry
46
+ * lexicon names, AND they must be at least {@link TIER_MAJORITY} of all
47
+ * buckets. One granularity-sounding folder among domain folders is a
48
+ * collision, not a convention — that is the false-positive direction the
49
+ * `collision` fixture pins.
50
+ * 3. **Anything else is {@link NO_TAXONOMY}.** A flat component folder, a
51
+ * missing component folder, an unreadable tree, a non-existent root: all
52
+ * record `none`, and the authoring step then behaves exactly as it does
53
+ * without this module.
54
+ *
55
+ * Detection never throws. A malformed doc, a permission error, or a missing
56
+ * directory degrades to `none` — the same recoverable-error contract as
57
+ * `stack/detect.ts`, and for the same reason: a wrong label is recoverable
58
+ * (the user can say so), a crash mid-dispatch is not. Degrading to `none` is
59
+ * also the *safe* direction here, because `none` is precisely the value that
60
+ * leaves the project's own structures untouched.
61
+ */
62
+ import * as fs from 'node:fs';
63
+ import * as path from 'node:path';
64
+
65
+ /** Recorded value for a project that evidences no granularity taxonomy. */
66
+ export const NO_TAXONOMY = 'none';
67
+
68
+ /**
69
+ * Granularity words used to RECOGNISE a tier name — never to prescribe one.
70
+ *
71
+ * Drawn from several vocabularies rather than one, and read at exactly one
72
+ * place: {@link _infer_tiers}, which calls `.has()` and nothing else. It
73
+ * carries no level, no cap and no placement rule, and it is not consulted when
74
+ * a component is placed. A project is free to use names that appear nowhere
75
+ * here — it declares them, and the declaration wins.
76
+ *
77
+ * **Held ALPHABETICALLY, and that is load-bearing rather than tidy.** A
78
+ * JavaScript `Set` iterates in INSERTION order, so "it is a Set" proves
79
+ * nothing about ordering — an earlier draft of this file claimed exactly that
80
+ * while the first five entries were one vocabulary's five levels in their own
81
+ * sequence. Alphabetical order cannot be a level sequence, it is checkable,
82
+ * and a test asserts it. Iteration order is still never read for a decision.
83
+ */
84
+ const _LEXICON_WORDS: ReadonlyArray<string> = [
85
+ 'atom',
86
+ 'block',
87
+ 'cell',
88
+ 'composite',
89
+ 'element',
90
+ 'layout',
91
+ 'molecule',
92
+ 'organism',
93
+ 'page',
94
+ 'part',
95
+ 'particle',
96
+ 'pattern',
97
+ 'primitive',
98
+ 'template',
99
+ 'widget',
100
+ ];
101
+
102
+ export const GRANULARITY_LEXICON: ReadonlySet<string> = new Set(_LEXICON_WORDS);
103
+
104
+ /**
105
+ * Sibling folders that hold no components of their own tier.
106
+ *
107
+ * Excluded from the majority DENOMINATOR, never from the matches. A project
108
+ * that keeps `hooks/`, `utils/` and `types/` next to its tiers has not stopped
109
+ * using its tiers, but {@link COMPONENT_SUFFIXES} includes `.ts`, so those
110
+ * folders read as buckets and drag the share down: `atoms / molecules /
111
+ * organisms / hooks / utils / types` measured 3 of 6 and recorded `none`.
112
+ * Three ordinary sibling folders were enough to suppress a real taxonomy.
113
+ */
114
+ export const SUPPORT_BUCKETS: ReadonlySet<string> = new Set([
115
+ 'constant',
116
+ 'context',
117
+ 'helper',
118
+ 'hook',
119
+ 'icon',
120
+ 'lib',
121
+ 'provider',
122
+ 'store',
123
+ 'style',
124
+ 'test',
125
+ 'type',
126
+ 'util',
127
+ ]);
128
+
129
+ /** Fewest tiers that can evidence a taxonomy. One name is a coincidence. */
130
+ export const MIN_TIERS = 2;
131
+
132
+ /**
133
+ * Share of a component root's buckets that must be tier-shaped to infer one.
134
+ *
135
+ * **Above two thirds, which is a measurement rather than a preference.** It
136
+ * sat at 0.6, and at 0.6 the ordinary kind-split `components/{layout, pages,
137
+ * forms}` measured 2 of 3 = 0.67 and was recorded as the taxonomy
138
+ * `layout/pages` — every `forms` component then a conformance gap, on a
139
+ * project that never chose a taxonomy at all. That is this roadmap's
140
+ * highest-ranked risk happening, so the floor is set above the 2-of-3 ratio
141
+ * that produced it. `layout`, `page`, `block`, `element`, `part` and `cell`
142
+ * are all ordinary folder names as well as granularity words, which is why a
143
+ * bare share is not enough on its own and {@link SUPPORT_BUCKETS} exists.
144
+ *
145
+ * It is NOT set higher: `atoms / molecules / organisms` beside one support
146
+ * folder is 3 of 4 = 0.75 and must still be found.
147
+ */
148
+ export const TIER_MAJORITY = 0.7;
149
+
150
+ /** Directory names searched, in order, for the project's component root. */
151
+ export const COMPONENT_ROOT_CANDIDATES: ReadonlyArray<string> = [
152
+ 'src/components',
153
+ 'app/components',
154
+ 'src/lib/components',
155
+ 'src/app/components',
156
+ 'resources/js/components',
157
+ 'resources/views/components',
158
+ 'app/javascript/components',
159
+ 'components',
160
+ ];
161
+
162
+ /**
163
+ * Docs read for a declared convention, in order; the first QUALIFYING one wins.
164
+ *
165
+ * A doc with no `## Component taxonomy` section, or one naming fewer than
166
+ * {@link MIN_TIERS} tiers that exist on disk, falls through to the next.
167
+ */
168
+ export const DECLARATION_DOCS: ReadonlyArray<string> = [
169
+ 'DESIGN.md',
170
+ 'docs/DESIGN.md',
171
+ ];
172
+
173
+ /** Heading that opens a declared-taxonomy section, lower-cased. */
174
+ const DECLARATION_HEADING = 'component taxonomy';
175
+
176
+ /** File suffixes that make a file look like a component. */
177
+ const COMPONENT_SUFFIXES: ReadonlyArray<string> = [
178
+ '.tsx',
179
+ '.jsx',
180
+ '.vue',
181
+ '.svelte',
182
+ '.blade.php',
183
+ '.ts',
184
+ '.js',
185
+ ];
186
+
187
+ /** Where the recorded taxonomy came from. */
188
+ export type TaxonomySource = 'declared' | 'inferred' | 'none';
189
+
190
+ /** What {@link detect_component_taxonomy} returns. */
191
+ export interface TaxonomyResult {
192
+ /**
193
+ * The single value the audit records in `state.ui_audit.component_taxonomy`
194
+ * — the project's own tiers joined with `/`, or {@link NO_TAXONOMY}.
195
+ */
196
+ taxonomy: string;
197
+ /**
198
+ * The same tiers as a list.
199
+ *
200
+ * `declared` carries the DOCUMENT's order, which is the project's own.
201
+ * `inferred` carries alphabetical order, because bucket names arrive
202
+ * sorted and no filesystem fact states a level sequence. It reads as one
203
+ * for `atoms/molecules/organisms` by coincidence and would not for
204
+ * `pages/templates/organisms`. Nothing downstream reads the order.
205
+ */
206
+ tiers: string[];
207
+ /** Component root relative to the project root, or `null` when none. */
208
+ component_root: string | null;
209
+ /** Which rule produced the answer. */
210
+ source: TaxonomySource;
211
+ /** Human-readable trace of what was read, for the audit's output. */
212
+ evidence: string[];
213
+ }
214
+
215
+ function _none(evidence: string[], component_root: string | null = null): TaxonomyResult {
216
+ return {
217
+ taxonomy: NO_TAXONOMY,
218
+ tiers: [],
219
+ component_root,
220
+ source: 'none',
221
+ evidence,
222
+ };
223
+ }
224
+
225
+ /**
226
+ * Detect the component-granularity taxonomy a project evidences.
227
+ *
228
+ * @param project_root Absolute or relative path to the project root.
229
+ * @returns A {@link TaxonomyResult}; never throws.
230
+ */
231
+ export function detect_component_taxonomy(project_root: string): TaxonomyResult {
232
+ const evidence: string[] = [];
233
+ let component_root: string | null;
234
+ try {
235
+ component_root = _find_component_root(project_root);
236
+ } catch {
237
+ return _none(['component root unreadable']);
238
+ }
239
+ if (component_root === null) {
240
+ evidence.push(
241
+ `no component root under ${_display(project_root)} ` +
242
+ `(searched ${COMPONENT_ROOT_CANDIDATES.length} conventional paths)`,
243
+ );
244
+ return _none(evidence);
245
+ }
246
+ evidence.push(`component root: ${component_root}`);
247
+
248
+ let buckets: string[];
249
+ try {
250
+ buckets = _component_buckets(path.join(project_root, component_root));
251
+ } catch {
252
+ return _none([...evidence, 'component root unreadable'], component_root);
253
+ }
254
+
255
+ // `_declared_tiers` already applies the MIN_TIERS floor and returns `[]`
256
+ // below it, so re-testing the floor here would be a second copy of one
257
+ // invariant — the shape this branch removed from `apply.ts` for masking a
258
+ // regression in its own sibling. One owner; this asks only "was there one".
259
+ const declared = _declared_tiers(project_root, buckets, evidence);
260
+ if (declared.length > 0) {
261
+ return {
262
+ taxonomy: declared.join('/'),
263
+ tiers: declared,
264
+ component_root,
265
+ source: 'declared',
266
+ evidence,
267
+ };
268
+ }
269
+
270
+ const inferred = _infer_tiers(buckets, evidence);
271
+ if (inferred.length === 0) {
272
+ return _none(evidence, component_root);
273
+ }
274
+ return {
275
+ taxonomy: inferred.join('/'),
276
+ tiers: inferred,
277
+ component_root,
278
+ source: 'inferred',
279
+ evidence,
280
+ };
281
+ }
282
+
283
+ /** First conventional component directory that exists, or `null`. */
284
+ function _find_component_root(project_root: string): string | null {
285
+ for (const candidate of COMPONENT_ROOT_CANDIDATES) {
286
+ const full = path.join(project_root, ...candidate.split('/'));
287
+ let st: fs.Stats;
288
+ try {
289
+ st = fs.statSync(full);
290
+ } catch {
291
+ continue;
292
+ }
293
+ if (st.isDirectory()) {
294
+ return candidate;
295
+ }
296
+ }
297
+ return null;
298
+ }
299
+
300
+ /**
301
+ * Immediate subdirectories of the component root that actually hold components.
302
+ *
303
+ * A directory with no component-shaped file anywhere beneath it is not a
304
+ * bucket — it is a stray folder, and counting it would drag the majority test
305
+ * around for reasons that have nothing to do with the project's convention.
306
+ */
307
+ function _component_buckets(root: string): string[] {
308
+ const out: string[] = [];
309
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
310
+ if (!entry.isDirectory()) continue;
311
+ if (entry.name.startsWith('.')) continue;
312
+ if (_holds_component(path.join(root, entry.name), 0)) {
313
+ out.push(entry.name);
314
+ }
315
+ }
316
+ out.sort();
317
+ return out;
318
+ }
319
+
320
+ /** True when a component-shaped file sits at or under `dir` (depth-capped). */
321
+ function _holds_component(dir: string, depth: number): boolean {
322
+ if (depth > 3) return false;
323
+ let entries: fs.Dirent[];
324
+ try {
325
+ entries = fs.readdirSync(dir, { withFileTypes: true });
326
+ } catch {
327
+ return false;
328
+ }
329
+ for (const entry of entries) {
330
+ if (entry.isFile()) {
331
+ if (COMPONENT_SUFFIXES.some((s) => entry.name.endsWith(s))) {
332
+ return true;
333
+ }
334
+ } else if (entry.isDirectory() && !entry.name.startsWith('.')) {
335
+ if (_holds_component(path.join(dir, entry.name), depth + 1)) {
336
+ return true;
337
+ }
338
+ }
339
+ }
340
+ return false;
341
+ }
342
+
343
+ /**
344
+ * Tiers declared in the project's own docs, grounded against the tree.
345
+ *
346
+ * Reads the `## Component taxonomy` section of the first
347
+ * {@link DECLARATION_DOCS} hit and keeps the backticked names that exist as
348
+ * buckets. Order is the document's, because that is the project's own order.
349
+ */
350
+ function _declared_tiers(
351
+ project_root: string,
352
+ buckets: string[],
353
+ evidence: string[],
354
+ ): string[] {
355
+ const known = new Set(buckets);
356
+ for (const doc of DECLARATION_DOCS) {
357
+ let body: string;
358
+ try {
359
+ body = fs.readFileSync(path.join(project_root, ...doc.split('/')), 'utf8');
360
+ } catch {
361
+ continue;
362
+ }
363
+ const section = _taxonomy_section(body);
364
+ if (section === null) {
365
+ evidence.push(`${doc}: no \`## Component taxonomy\` section`);
366
+ continue;
367
+ }
368
+ const named: string[] = [];
369
+ for (const line of section.split(/\r?\n/u)) {
370
+ // Only a list item declares a tier. A backticked name inside a
371
+ // prose sentence is a mention — the `legacy` fixture says in prose
372
+ // that it is NOT a tier, and a parser that scanned the whole
373
+ // section would enrol it on the strength of the backticks alone.
374
+ // `**` / `__` may lead the name: `- **\`alpha\`** — ...` is a
375
+ // completely ordinary way to write this list, and a regex that
376
+ // demanded the backtick lead dropped it silently.
377
+ const item = /^\s*(?:[-*+]|\d+[.)])\s+(?:\*\*|__)?`([^`\n]+)`/u.exec(line);
378
+ if (item === null) continue;
379
+ const name = (item[1] ?? '').trim();
380
+ // Grounded against the tree: a declaration cannot conjure a tier
381
+ // the project has not created.
382
+ if (name !== '' && known.has(name) && !named.includes(name)) {
383
+ named.push(name);
384
+ }
385
+ }
386
+ if (named.length >= MIN_TIERS) {
387
+ evidence.push(`${doc}: declares ${named.length} tiers, all present on disk`);
388
+ return named;
389
+ }
390
+ evidence.push(
391
+ `${doc}: \`## Component taxonomy\` names ${named.length} tier(s) ` +
392
+ `present on disk — below the ${MIN_TIERS}-tier floor, so not a declaration`,
393
+ );
394
+ }
395
+ return [];
396
+ }
397
+
398
+ /** Body of the `## Component taxonomy` section, or `null` when absent. */
399
+ function _taxonomy_section(body: string): string | null {
400
+ const lines = body.split(/\r?\n/u);
401
+ let start = -1;
402
+ let level = 0;
403
+ for (let i = 0; i < lines.length; i += 1) {
404
+ const m = /^(#{1,6})\s+(.*)$/u.exec(lines[i] as string);
405
+ if (m === null) continue;
406
+ const heading = (m[2] ?? '').trim().toLowerCase().replace(/[:.]+$/u, '');
407
+ if (start === -1) {
408
+ if (heading === DECLARATION_HEADING) {
409
+ start = i + 1;
410
+ level = (m[1] as string).length;
411
+ }
412
+ continue;
413
+ }
414
+ if ((m[1] as string).length <= level) {
415
+ return lines.slice(start, i).join('\n');
416
+ }
417
+ }
418
+ return start === -1 ? null : lines.slice(start).join('\n');
419
+ }
420
+
421
+ /**
422
+ * Tiers inferred from bucket names, or `[]` when the evidence is too thin.
423
+ *
424
+ * The only place {@link GRANULARITY_LEXICON} is consulted.
425
+ */
426
+ function _infer_tiers(buckets: string[], evidence: string[]): string[] {
427
+ if (buckets.length === 0) {
428
+ evidence.push('component root has no sub-directories — flat layout');
429
+ return [];
430
+ }
431
+ const matched = buckets.filter((b) => GRANULARITY_LEXICON.has(_singular(b)));
432
+ const counted = buckets.filter((b) => !SUPPORT_BUCKETS.has(_singular(b)));
433
+ if (counted.length === 0) {
434
+ evidence.push('every bucket is a support folder — no tiers to weigh');
435
+ return [];
436
+ }
437
+ const share = matched.length / counted.length;
438
+ if (matched.length < MIN_TIERS || share < TIER_MAJORITY) {
439
+ evidence.push(
440
+ `${matched.length} of ${counted.length} weighed buckets carry granularity ` +
441
+ `names (need >= ${MIN_TIERS} and >= ${Math.round(TIER_MAJORITY * 100)}%; ` +
442
+ `${buckets.length - counted.length} support folder(s) not weighed) — ` +
443
+ 'reads as domain folders, not a taxonomy',
444
+ );
445
+ return [];
446
+ }
447
+ evidence.push(
448
+ `${matched.length} of ${counted.length} weighed buckets carry granularity ` +
449
+ `names (${buckets.length - counted.length} support folder(s) not weighed) — ` +
450
+ 'inferred from layout',
451
+ );
452
+ return matched;
453
+ }
454
+
455
+ /**
456
+ * Lower-case a bucket name and drop a trailing plural `s`.
457
+ *
458
+ * Deliberately naive: it exists so `atoms` matches `atom`, not to be a
459
+ * morphology engine. A name it mis-normalises simply fails to match, which
460
+ * lands on `none` — the safe direction.
461
+ */
462
+ function _singular(name: string): string {
463
+ const lower = name.toLowerCase();
464
+ return lower.endsWith('s') && lower.length > 2 ? lower.slice(0, -1) : lower;
465
+ }
466
+
467
+ /** Trim a path for an evidence line so the trace stays readable. */
468
+ function _display(p: string): string {
469
+ const base = path.basename(p);
470
+ return base === '' ? p : base;
471
+ }
472
+
473
+ /**
474
+ * CLI entry: print the detection result for a project root as JSON.
475
+ *
476
+ * `existing-ui-audit` § 1b tells the agent to RUN this file, so it has to be
477
+ * runnable. Without this block `npx tsx .../detect.ts` exited 0 and printed
478
+ * nothing, and an agent following the instruction literally got silence and
479
+ * then had to invent the two state keys the audit is supposed to record.
480
+ *
481
+ * ```bash
482
+ * npx tsx scripts/work_engine/taxonomy/detect.ts [--root <dir>]
483
+ * ```
484
+ *
485
+ * Defaults to the process CWD. Exits 0 whichever answer it reaches: `none` is
486
+ * a real answer here, not a failure, and an exit code that said otherwise
487
+ * would push a caller toward treating a no-taxonomy project as an error.
488
+ */
489
+ export function main(argv: ReadonlyArray<string>): number {
490
+ const at = argv.indexOf('--root');
491
+ const root = at !== -1 && at + 1 < argv.length ? (argv[at + 1] as string) : process.cwd();
492
+ process.stdout.write(`${JSON.stringify(detect_component_taxonomy(root), null, 2)}\n`);
493
+ return 0;
494
+ }
495
+
496
+ // Only when this module is the process entry, never on import. tsx sets
497
+ // `import.meta.url` to the entry file URL.
498
+ if (import.meta.url === `file://${process.argv[1]}`) {
499
+ process.exitCode = main(process.argv.slice(2));
500
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Placing a new component inside the taxonomy a project already chose.
3
+ *
4
+ * Leaf module — no intra-`work_engine` imports beyond its sibling detector's
5
+ * `NO_TAXONOMY` constant. That one import does pull `node:fs` in transitively;
6
+ * "stdlib-free" was the old wording here and it was simply wrong. Public API names stay snake_case to
7
+ * mirror the sibling detectors 1:1, per ADR-200.
8
+ *
9
+ * The contract, and the reason it is this small: placement reads the RECORDED
10
+ * tier list by name and conforms to whatever it holds. There is no table of
11
+ * tier names here, no ordering, no per-tier rule, and no numeric budget keyed
12
+ * to a tier — a component is placed when the brief names a tier the project
13
+ * has, and is reported as a gap when it does not. Anything richer would be the
14
+ * compiled-in taxonomy this suite refused to hard-code, arriving as an
15
+ * implementation detail rather than as a decision.
16
+ *
17
+ * That is also why an unplaceable component is never quietly put somewhere
18
+ * else. Choosing a tier for it would need exactly the per-tier rule the line
19
+ * above rules out, so the honest output is a named gap the author resolves.
20
+ *
21
+ * With {@link NO_TAXONOMY} recorded — the value for a project that evidences
22
+ * no taxonomy — every plan comes back empty and {@link conformance_lines}
23
+ * returns nothing, so the authoring step's output is byte-identical to what it
24
+ * was before this module existed. Holding to the project's own structures is
25
+ * the standing rule; a project that organises its components some other way
26
+ * must not be able to tell that this module shipped.
27
+ */
28
+ import { NO_TAXONOMY } from './detect.js';
29
+
30
+ /** A component the design brief asks for. */
31
+ export interface ComponentRequest {
32
+ /** The component's name, as the brief writes it. */
33
+ name?: unknown;
34
+ /** The granularity tier the brief assigns it, when it assigns one. */
35
+ tier?: unknown;
36
+ }
37
+
38
+ /** Where one component lands, or why it could not be placed. */
39
+ export interface PlacementPlan {
40
+ /** The component's name as read from the brief. */
41
+ name: string;
42
+ /** The project's own tier name it was matched to, or `null`. */
43
+ tier: string | null;
44
+ /** The directory it belongs in, or `null` when unplaced. */
45
+ directory: string | null;
46
+ /** Why it could not be placed, or `null` when it was. */
47
+ gap: string | null;
48
+ }
49
+
50
+ /** Component root used when the audit recorded none. */
51
+ /**
52
+ * Gap reported when a taxonomy is recorded without the root it lives under.
53
+ *
54
+ * Guessing `src/components` here is the same failure as placing a component in
55
+ * a tier the project does not have, at the other end of the path: a project
56
+ * rooted at `app/components` or `resources/js/components` — both of which the
57
+ * detector searches — would have been handed a directory that does not exist,
58
+ * with no gap and no warning. This module exists so nothing is placed
59
+ * somewhere the project did not choose, so it says so instead.
60
+ */
61
+ export const MISSING_ROOT_GAP =
62
+ 'the audit recorded a taxonomy but no `component_root`, so there is no ' +
63
+ 'directory to place into — re-run the audit, or record the root by hand';
64
+
65
+ /**
66
+ * Plan where each requested component belongs under a recorded taxonomy.
67
+ *
68
+ * @param taxonomy The recorded `state.ui_audit.component_taxonomy` value —
69
+ * the project's own tiers joined with `/`, or {@link NO_TAXONOMY}.
70
+ * @param component_root The recorded component root, relative to the project.
71
+ * @param components The components the design brief asks for.
72
+ */
73
+ export function plan_component_placement(
74
+ taxonomy: string,
75
+ component_root: string,
76
+ components: ReadonlyArray<ComponentRequest>,
77
+ ): PlacementPlan[] {
78
+ const tiers = read_tiers(taxonomy);
79
+ const root = _trim_slashes(component_root);
80
+ return components.map((component) => {
81
+ const name = _text(component.name) || '(unnamed)';
82
+ if (tiers.length === 0) {
83
+ return { name, tier: null, directory: null, gap: null };
84
+ }
85
+ if (root === '') {
86
+ return { name, tier: null, directory: null, gap: `\`${name}\` — ${MISSING_ROOT_GAP}` };
87
+ }
88
+ const hint = _text(component.tier);
89
+ if (hint === '') {
90
+ return {
91
+ name,
92
+ tier: null,
93
+ directory: null,
94
+ gap:
95
+ `\`${name}\` names no granularity tier, and the project's ` +
96
+ `taxonomy \`${taxonomy}\` carries no rule that assigns one ` +
97
+ '— left unplaced for the author to decide',
98
+ };
99
+ }
100
+ const matched = tiers.find((tier) => _normalise(tier) === _normalise(hint));
101
+ if (matched === undefined) {
102
+ return {
103
+ name,
104
+ tier: null,
105
+ directory: null,
106
+ gap:
107
+ `\`${name}\` declares tier \`${hint}\`, which the project's ` +
108
+ `taxonomy \`${taxonomy}\` does not contain — left unplaced ` +
109
+ 'rather than moved somewhere the project did not choose',
110
+ };
111
+ }
112
+ return { name, tier: matched, directory: `${root}/${matched}`, gap: null };
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Split a recorded taxonomy value into the project's own tier names.
118
+ *
119
+ * **The sentinel comparison is case-insensitive, and that is not politeness.**
120
+ * This field is written by an agent following `existing-ui-audit` § 1b, so
121
+ * `'None'` is reachable, and an exact comparison let it through as a
122
+ * one-element tier list: a project with no taxonomy then got a "conforming to
123
+ * `None`" banner and a conformance gap per component — precisely the output
124
+ * AC-2 forbids. It is the one guard standing between a no-taxonomy project and
125
+ * a changed authoring step, so it refuses every spelling of the sentinel.
126
+ */
127
+ export function read_tiers(taxonomy: string): string[] {
128
+ const value = _text(taxonomy);
129
+ if (value === '' || value.toLowerCase() === NO_TAXONOMY) {
130
+ return [];
131
+ }
132
+ return value
133
+ .split('/')
134
+ .map((part) => part.trim())
135
+ .filter((part) => part !== '');
136
+ }
137
+
138
+ /**
139
+ * Render the lines the authoring step says out loud.
140
+ *
141
+ * Empty for {@link NO_TAXONOMY}, which is what keeps a no-taxonomy project's
142
+ * output unchanged.
143
+ */
144
+ export function conformance_lines(
145
+ taxonomy: string,
146
+ component_root: string,
147
+ plans: ReadonlyArray<PlacementPlan>,
148
+ ): string[] {
149
+ if (read_tiers(taxonomy).length === 0) {
150
+ return [];
151
+ }
152
+ const root = _trim_slashes(component_root);
153
+ const under = root === '' ? 'an unrecorded component root' : `\`${root}/\``;
154
+ const lines: string[] = [
155
+ `> Conforming to the project's own component taxonomy ` +
156
+ `\`${taxonomy}\` under ${under} — detected, not imposed. ` +
157
+ 'A component that fits no tier is reported, never relocated.',
158
+ ];
159
+ for (const plan of plans) {
160
+ if (plan.gap === null && plan.directory !== null) {
161
+ lines.push(
162
+ `> - \`${plan.name}\` → \`${plan.directory}/\` (tier \`${plan.tier}\`)`,
163
+ );
164
+ } else if (plan.gap !== null) {
165
+ lines.push(`> - conformance gap: ${plan.gap}`);
166
+ }
167
+ }
168
+ return lines;
169
+ }
170
+
171
+ /** Lower-case and drop a trailing plural `s`, so `Molecule` matches `molecules`. */
172
+ function _normalise(name: string): string {
173
+ const lower = name.trim().toLowerCase();
174
+ return lower.endsWith('s') && lower.length > 2 ? lower.slice(0, -1) : lower;
175
+ }
176
+
177
+ function _text(value: unknown): string {
178
+ return typeof value === 'string' ? value.trim() : '';
179
+ }
180
+
181
+ function _trim_slashes(value: unknown): string {
182
+ return _text(value).replace(/^\/+|\/+$/gu, '');
183
+ }