@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
|
@@ -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
|
+
}
|