@event4u/agent-config 9.11.0 → 9.13.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +80 -204
- package/README.md +8 -1
- package/dist/agent-src/commands/fix/ci.md +29 -0
- package/dist/agent-src/commands/pr/create.md +60 -0
- package/dist/agent-src/contexts/authority/kernel-rule-edits.md +18 -2
- package/dist/agent-src/rules/active-remediation.md +0 -2
- package/dist/agent-src/rules/architecture.md +0 -1
- package/dist/agent-src/rules/artifact-drafting-protocol.md +0 -4
- package/dist/agent-src/rules/autonomous-execution.md +1 -2
- package/dist/agent-src/rules/broken-access-control.md +0 -2
- package/dist/agent-src/rules/code-comment-discipline.md +0 -3
- package/dist/agent-src/rules/code-provenance.md +0 -3
- package/dist/agent-src/rules/communication-through-line.md +2 -3
- package/dist/agent-src/rules/content-quoting-floor.md +0 -2
- package/dist/agent-src/rules/context-hygiene.md +1 -3
- package/dist/agent-src/rules/decision-revisit-gate.md +0 -2
- package/dist/agent-src/rules/design-fidelity.md +30 -1
- package/dist/agent-src/rules/domain-adoption-policy.md +0 -3
- package/dist/agent-src/rules/downstream-changes.md +10 -1
- package/dist/agent-src/rules/external-reference-deep-dive.md +0 -7
- package/dist/agent-src/rules/fast-path-marker-visibility.md +0 -1
- package/dist/agent-src/rules/git-history-discipline.md +0 -7
- package/dist/agent-src/rules/guidelines.md +0 -2
- package/dist/agent-src/rules/improve-before-implement.md +4 -2
- package/dist/agent-src/rules/invite-challenge.md +0 -4
- package/dist/agent-src/rules/linked-projects-onboarding-gate.md +0 -2
- package/dist/agent-src/rules/markdown-safe-codeblocks.md +0 -1
- package/dist/agent-src/rules/minimal-safe-diff.md +2 -2
- package/dist/agent-src/rules/missing-tool-handling.md +0 -1
- package/dist/agent-src/rules/no-attribution-footers.md +0 -3
- package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +8 -5
- package/dist/agent-src/rules/no-pr-progress-comments.md +1 -3
- package/dist/agent-src/rules/no-roadmap-references.md +0 -2
- package/dist/agent-src/rules/output-discipline.md +0 -1
- package/dist/agent-src/rules/prefer-enums-over-literals.md +0 -2
- package/dist/agent-src/rules/preservation-guard.md +0 -3
- package/dist/agent-src/rules/role-mode-adherence.md +0 -1
- package/dist/agent-src/rules/secret-vcs-guard.md +0 -2
- package/dist/agent-src/rules/senior-engineering-discipline.md +0 -2
- package/dist/agent-src/rules/session-canary.md +0 -2
- package/dist/agent-src/rules/size-enforcement.md +0 -5
- package/dist/agent-src/rules/source-confidentiality.md +0 -3
- package/dist/agent-src/rules/source-discovery-gate.md +0 -2
- package/dist/agent-src/rules/think-before-action.md +3 -3
- package/dist/agent-src/rules/token-budget-discipline.md +0 -1
- package/dist/agent-src/rules/token-efficiency.md +1 -2
- package/dist/agent-src/rules/tool-safety.md +0 -1
- package/dist/agent-src/rules/user-interaction.md +0 -3
- package/dist/agent-src/rules/user-interrupt-priority.md +0 -1
- package/dist/agent-src/skills/agent-docs-writing/SKILL.md +8 -0
- package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +1 -0
- package/dist/agent-src/skills/analysis-skill-router/SKILL.md +55 -86
- package/dist/agent-src/skills/character-consistency/SKILL.md +7 -4
- package/dist/agent-src/skills/decision-record/SKILL.md +8 -0
- package/dist/agent-src/skills/design-intelligence/SKILL.md +27 -1
- package/dist/agent-src/skills/design-intelligence/data/font-pairings-reference.csv +74 -74
- package/dist/agent-src/skills/design-intelligence/data/manifest.json +2 -0
- package/dist/agent-src/skills/design-review/SKILL.md +11 -0
- package/dist/agent-src/skills/design-system-capture/reference/design-system-json.md +8 -0
- package/dist/agent-src/skills/design-tokens/scripts/tokens.ts +0 -7
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +34 -3
- package/dist/agent-src/skills/fe-design/SKILL.md +10 -0
- package/dist/agent-src/skills/motion-choreographer/SKILL.md +7 -4
- package/dist/agent-src/skills/overbuild-review-lens/SKILL.md +201 -0
- package/dist/agent-src/skills/performance/SKILL.md +14 -0
- package/dist/agent-src/skills/pixar-storyteller/SKILL.md +8 -5
- package/dist/agent-src/skills/scene-expander/SKILL.md +9 -6
- package/dist/agent-src/skills/supply-chain-intake/SKILL.md +10 -0
- package/dist/agent-src/skills/typography-system/SKILL.md +55 -11
- package/dist/agent-src/skills/ui-apply-generic/SKILL.md +180 -0
- package/dist/agent-src/skills/universal-project-analysis/SKILL.md +14 -37
- package/dist/agent-src/skills/video-director/SKILL.md +9 -6
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +162 -17
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +118 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +49 -6
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/review.ts +9 -5
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/stack_bundles.ts +182 -29
- package/dist/agent-src/templates/scripts/work_engine/stack/detect.ts +311 -24
- package/dist/agent-src/templates/scripts/work_engine/state.ts +55 -0
- package/dist/cli-delegate/chunk-523HCFYO.js +90 -0
- package/dist/cli-delegate/{chunk-IR7P7JWA.js → chunk-A3HJ6NTI.js} +271 -59
- package/dist/cli-delegate/chunk-EJYIGZ3T.js +125 -0
- package/dist/cli-delegate/{chunk-RDGRHAAB.js → chunk-HYWSRNUX.js} +6 -86
- package/dist/cli-delegate/{chunk-YMFO6FDN.js → chunk-QFKQLQ7X.js} +313 -81
- package/dist/cli-delegate/{chunk-4SV3XGFA.js → chunk-TEGJU2JX.js} +7 -9
- package/dist/cli-delegate/chunk-TJIAM5FW.js +133 -0
- package/dist/cli-delegate/cmd_conformance.js +4 -7
- package/dist/cli-delegate/cmd_doctor.js +3 -5
- package/dist/cli-delegate/cmd_explain.js +153 -85
- package/dist/cli-delegate/cmd_export.js +5 -3
- package/dist/cli-delegate/cmd_fleet.js +2 -1
- package/dist/cli-delegate/cmd_preflight.js +6 -123
- package/dist/cli-delegate/cmd_prune.js +5 -3
- package/dist/cli-delegate/cmd_settings_migrate.js +5 -3
- package/dist/cli-delegate/cmd_sync.js +5 -3
- package/dist/cli-delegate/cmd_uninstall.js +6 -5
- package/dist/cli-delegate/cmd_validate.js +5 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +116 -72
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +7 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +6 -4
- package/dist/hooks/dispatch.js +174 -13
- package/dist/install/install.mjs +605 -412
- package/dist/install/ruleInScope.js +123 -0
- package/dist/install/ruleInScope.js.map +1 -0
- package/dist/install/rule_scope.js +2 -2
- package/dist/install/rule_scope.js.map +1 -1
- package/dist/install/wizard-plan.js +10 -1
- package/dist/install/wizard-plan.js.map +1 -1
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +710 -2491
- package/dist/router.json +1 -1
- package/dist/scripts/condense.js +8 -24
- package/dist/scripts/condense.js.map +1 -1
- package/docs/MIGRATION.md +1 -1
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +71 -0
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +24 -1
- package/docs/guidelines/code-clarity.md +9 -2
- package/docs/guidelines/design-fidelity-mechanics.md +58 -0
- package/llms.txt +63 -27
- package/package.json +4 -4
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +162 -17
- package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +118 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +49 -6
- package/src/agent-src/templates/scripts/work_engine/directives/ui/review.ts +9 -5
- package/src/agent-src/templates/scripts/work_engine/directives/ui/stack_bundles.ts +182 -29
- package/src/agent-src/templates/scripts/work_engine/stack/detect.ts +311 -24
- package/src/agent-src/templates/scripts/work_engine/state.ts +55 -0
- package/src/config/agent-settings.template.yml +8 -0
- package/src/install/ruleInScope.ts +133 -0
- package/src/install/rule_scope.ts +2 -2
- package/src/install/wizard-plan.ts +11 -1
- package/src/scripts/_lib/counted_probe.ts +142 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +38 -2
- package/src/scripts/_lib/overbuild_lens_contract.ts +196 -0
- package/src/scripts/_lib/release_scope.ts +207 -0
- package/src/scripts/ai_council/stance_tally.ts +37 -2
- package/src/scripts/audit_auto_rules.ts +9 -9
- package/src/scripts/audit_likelihood.ts +3 -3
- package/src/scripts/audit_overlap.ts +2 -2
- package/src/scripts/audit_skill_overlap.ts +233 -95
- package/src/scripts/audit_skill_overlap_allowlist.json +4 -0
- package/src/scripts/build_proof.ts +15 -1
- package/src/scripts/check_dependency_floors.ts +16 -6
- package/src/scripts/check_gate_coverage.ts +6 -6
- package/src/scripts/check_installer_import_purity.ts +295 -0
- package/src/scripts/check_kernel_rule_bundle.ts +5 -4
- package/src/scripts/check_token_quality_golden.ts +0 -18
- package/src/scripts/code_graph/loader.ts +35 -6
- package/src/scripts/compile_router.ts +3 -8
- package/src/scripts/condense.ts +9 -30
- package/src/scripts/explain_run.ts +0 -1
- package/src/scripts/generate_pack_manifests.ts +3 -20
- package/src/scripts/hooks/block_kernel_rule_writes.ts +145 -10
- package/src/scripts/hooks/block_no_verify.ts +101 -0
- package/src/scripts/install-hooks.sh +36 -4
- package/src/scripts/install.ts +210 -5
- package/src/scripts/lint_agent_security.ts +5 -4
- package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
- package/src/scripts/lint_regression.ts +92 -1
- package/src/scripts/lint_skill_descriptions.ts +93 -5
- package/src/scripts/lint_ui_stack_bundles.ts +31 -0
- package/src/scripts/project_thin_rules.ts +2 -2
- package/src/scripts/release.ts +107 -3
- package/src/scripts/resolve_lint_scope.ts +53 -0
- package/src/scripts/router_telemetry.ts +0 -5
- package/src/scripts/runtime_dispatcher.ts +40 -0
- package/src/scripts/schemas/pack.schema.json +1 -5
- package/src/scripts/schemas/rule.schema.json +1 -2
- package/src/scripts/self_review_gate.ts +15 -73
- package/src/scripts/skill_linter.ts +93 -11
- package/src/scripts/sweep_dead_scan_roots.ts +550 -0
- package/src/scripts/trigger_coverage.ts +11 -37
- package/dist/cli-delegate/chunk-7DSAMZFU.js +0 -356
- package/dist/cli-delegate/chunk-BJGXZNPM.js +0 -266
- package/dist/cli-delegate/chunk-TFG57JJM.js +0 -1969
|
@@ -122,6 +122,49 @@ const _UNMODELLED_MARKERS: ReadonlyArray<string> = [
|
|
|
122
122
|
*/
|
|
123
123
|
type Manifest = { [key: string]: unknown };
|
|
124
124
|
|
|
125
|
+
/**
|
|
126
|
+
* The independent axes a frontend stack actually varies along.
|
|
127
|
+
*
|
|
128
|
+
* A flat label forces all-or-nothing matches: `livewire ∧ flux` was never the
|
|
129
|
+
* bug, it was the symptom of collapsing three axes into one enum value. The
|
|
130
|
+
* measured proof is Nuxt — `nuxt` is on the unmodelled-marker list, but the
|
|
131
|
+
* project is labelled `vue` because Vue is a Nuxt dependency and matched first.
|
|
132
|
+
* No ordering of one list can express "Nuxt implies Vue but is not Vue".
|
|
133
|
+
*
|
|
134
|
+
* Axes are **additive**: the flat `frontend` label keeps its existing
|
|
135
|
+
* computation untouched, so the eight shipped labels are byte-stable by
|
|
136
|
+
* construction rather than by test. Dispatch composition reads the axes.
|
|
137
|
+
*
|
|
138
|
+
* `'none'` means "this axis is genuinely absent" (a Blade app has no JS
|
|
139
|
+
* component library); `'unknown'` means "not recognised", which is a different
|
|
140
|
+
* fact and must not be conflated with absence.
|
|
141
|
+
*/
|
|
142
|
+
export interface StackAxes {
|
|
143
|
+
/** Template / view layer — what markup is authored in. */
|
|
144
|
+
readonly view: string;
|
|
145
|
+
/** Reactivity layer — what drives updates. */
|
|
146
|
+
readonly reactivity: string;
|
|
147
|
+
/** Component library on top of the reactivity layer. */
|
|
148
|
+
readonly component_lib: string;
|
|
149
|
+
/** Styling system. */
|
|
150
|
+
readonly css: string;
|
|
151
|
+
/**
|
|
152
|
+
* Meta-framework wrapping the reactivity layer (Nuxt over Vue, Next over
|
|
153
|
+
* React, Astro over anything). Its own axis precisely because it does not
|
|
154
|
+
* replace the layer underneath — which is what the flat label got wrong.
|
|
155
|
+
*/
|
|
156
|
+
readonly meta: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Every axis unresolved — the shape for a project with no signals at all. */
|
|
160
|
+
const _EMPTY_AXES: StackAxes = {
|
|
161
|
+
view: 'none',
|
|
162
|
+
reactivity: 'none',
|
|
163
|
+
component_lib: 'none',
|
|
164
|
+
css: 'none',
|
|
165
|
+
meta: 'none',
|
|
166
|
+
};
|
|
167
|
+
|
|
125
168
|
/**
|
|
126
169
|
* Outcome of one detection pass.
|
|
127
170
|
*
|
|
@@ -138,10 +181,28 @@ type Manifest = { [key: string]: unknown };
|
|
|
138
181
|
export class StackResult {
|
|
139
182
|
readonly frontend: string;
|
|
140
183
|
readonly mtime: number;
|
|
141
|
-
|
|
142
|
-
|
|
184
|
+
/** Multi-axis view of the same detection. See {@link StackAxes}. */
|
|
185
|
+
readonly axes: StackAxes;
|
|
186
|
+
/**
|
|
187
|
+
* Conflicting signals on one axis, e.g. `['reactivity: react + vue']`.
|
|
188
|
+
*
|
|
189
|
+
* Non-empty means the project is genuinely two things and the detector
|
|
190
|
+
* refuses to pick. Guessing a priority order is the worse property for a
|
|
191
|
+
* global package: asking costs one turn, a wrong silent pick costs the
|
|
192
|
+
* whole run.
|
|
193
|
+
*/
|
|
194
|
+
readonly ambiguity: ReadonlyArray<string>;
|
|
195
|
+
|
|
196
|
+
constructor(args: {
|
|
197
|
+
frontend: string;
|
|
198
|
+
mtime: number;
|
|
199
|
+
axes?: StackAxes;
|
|
200
|
+
ambiguity?: ReadonlyArray<string>;
|
|
201
|
+
}) {
|
|
143
202
|
this.frontend = args.frontend;
|
|
144
203
|
this.mtime = args.mtime;
|
|
204
|
+
this.axes = args.axes ?? _EMPTY_AXES;
|
|
205
|
+
this.ambiguity = args.ambiguity ?? [];
|
|
145
206
|
}
|
|
146
207
|
}
|
|
147
208
|
|
|
@@ -164,34 +225,50 @@ export function detect_stack(project_root: string): StackResult {
|
|
|
164
225
|
const components_json = _is_file(path.join(project_root, 'components.json'));
|
|
165
226
|
const mtime = latest_manifest_mtime(project_root);
|
|
166
227
|
|
|
228
|
+
// Axes are computed once and attached to every result below. The label
|
|
229
|
+
// chain that follows is UNCHANGED — that is deliberate, so the eight
|
|
230
|
+
// shipped labels stay byte-stable by construction rather than by test.
|
|
231
|
+
// Composition reads `axes`; only the ambiguity guard alters a label.
|
|
232
|
+
const axes = _detect_axes(composer, pkg, components_json, project_root);
|
|
233
|
+
const ambiguity = _detect_ambiguity(composer, pkg);
|
|
234
|
+
const _r = (frontend: string): StackResult =>
|
|
235
|
+
new StackResult({ frontend, mtime, axes, ambiguity });
|
|
236
|
+
|
|
237
|
+
// Conflicting signals on one axis: refuse rather than let the priority
|
|
238
|
+
// chain below silently pick a winner. Measured baseline: react + vue in
|
|
239
|
+
// one manifest resolved to `react` with no warning at all.
|
|
240
|
+
if (ambiguity.length > 0) {
|
|
241
|
+
return _r(UNSUPPORTED_STACK);
|
|
242
|
+
}
|
|
243
|
+
|
|
167
244
|
if (_is_blade_livewire_flux(composer)) {
|
|
168
|
-
return
|
|
245
|
+
return _r('blade-livewire-flux');
|
|
169
246
|
}
|
|
170
247
|
|
|
171
248
|
// Filament before bare Livewire: it pulls Livewire in transitively, so a
|
|
172
249
|
// Filament project would otherwise be labelled by its dependency rather
|
|
173
250
|
// than by the framework the developer actually works in.
|
|
174
251
|
if (_has_package(composer, ['require', 'require-dev'], _FILAMENT_PACKAGE)) {
|
|
175
|
-
return
|
|
252
|
+
return _r('filament');
|
|
176
253
|
}
|
|
177
254
|
|
|
178
255
|
// Livewire WITHOUT Flux is the common Laravel frontend, not an unknown
|
|
179
256
|
// one. It used to fail the `&&` above and land in `plain`.
|
|
180
257
|
if (_has_package(composer, ['require', 'require-dev'], _LIVEWIRE_PACKAGE)) {
|
|
181
|
-
return
|
|
258
|
+
return _r('blade-livewire');
|
|
182
259
|
}
|
|
183
260
|
|
|
184
261
|
if (_is_react_shadcn(pkg, components_json)) {
|
|
185
|
-
return
|
|
262
|
+
return _r('react-shadcn');
|
|
186
263
|
}
|
|
187
264
|
|
|
188
265
|
// React without Radix/shadcn is a served stack, not an unknown one.
|
|
189
266
|
if (_has_package(pkg, _PKG_DEP_KEYS, 'react')) {
|
|
190
|
-
return
|
|
267
|
+
return _r('react');
|
|
191
268
|
}
|
|
192
269
|
|
|
193
270
|
if (_has_vue(pkg)) {
|
|
194
|
-
return
|
|
271
|
+
return _r('vue');
|
|
195
272
|
}
|
|
196
273
|
|
|
197
274
|
// A recognised-but-unmodelled framework is NOT `plain`. Refusing here is
|
|
@@ -199,7 +276,7 @@ export function detect_stack(project_root: string): StackResult {
|
|
|
199
276
|
// is what let a Svelte or Inertia project receive Tailwind-only tooling
|
|
200
277
|
// and no warning.
|
|
201
278
|
if (_has_unmodelled_marker(composer, pkg)) {
|
|
202
|
-
return
|
|
279
|
+
return _r(UNSUPPORTED_STACK);
|
|
203
280
|
}
|
|
204
281
|
|
|
205
282
|
// Monorepo: no manifest at the root, but one sits in a workspace directory.
|
|
@@ -209,11 +286,36 @@ export function detect_stack(project_root: string): StackResult {
|
|
|
209
286
|
//
|
|
210
287
|
// A repo with no manifest ANYWHERE stays `plain`: that is greenfield, and
|
|
211
288
|
// the scaffold path depends on it.
|
|
212
|
-
if (mtime === 0.0
|
|
213
|
-
|
|
289
|
+
if (mtime === 0.0) {
|
|
290
|
+
const roots = _nested_frontend_roots(project_root);
|
|
291
|
+
if (roots.length === 1) {
|
|
292
|
+
// Unambiguous scope: descend once and keep the workspace's own
|
|
293
|
+
// axes. Refusing here would punish the common monorepo shape for a
|
|
294
|
+
// scope decision the layout already makes.
|
|
295
|
+
const inner = detect_stack(roots[0] as string);
|
|
296
|
+
return new StackResult({
|
|
297
|
+
frontend: inner.frontend,
|
|
298
|
+
mtime: inner.mtime,
|
|
299
|
+
axes: inner.axes,
|
|
300
|
+
ambiguity: inner.ambiguity,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
if (roots.length > 1) {
|
|
304
|
+
// Several frontend roots: the caller owns scope selection, so name
|
|
305
|
+
// them and let the step ask. Picking the first would be the silent
|
|
306
|
+
// guess this detector exists to avoid.
|
|
307
|
+
return new StackResult({
|
|
308
|
+
frontend: UNSUPPORTED_STACK,
|
|
309
|
+
mtime,
|
|
310
|
+
axes,
|
|
311
|
+
ambiguity: [
|
|
312
|
+
`workspace roots: ${roots.map((r) => path.basename(r)).join(' + ')}`,
|
|
313
|
+
],
|
|
314
|
+
});
|
|
315
|
+
}
|
|
214
316
|
}
|
|
215
317
|
|
|
216
|
-
return
|
|
318
|
+
return _r(DEFAULT_STACK);
|
|
217
319
|
}
|
|
218
320
|
|
|
219
321
|
/**
|
|
@@ -226,9 +328,8 @@ export function detect_stack(project_root: string): StackResult {
|
|
|
226
328
|
* sentinel rather than a missing-file error.
|
|
227
329
|
*/
|
|
228
330
|
export function latest_manifest_mtime(project_root: string): number {
|
|
229
|
-
const candidates = ['composer.json', 'package.json'];
|
|
230
331
|
const mtimes: number[] = [];
|
|
231
|
-
for (const name of
|
|
332
|
+
for (const name of _CACHE_KEY_FILES) {
|
|
232
333
|
const p = path.join(project_root, name);
|
|
233
334
|
if (_is_file(p)) {
|
|
234
335
|
mtimes.push(_stat_mtime(p));
|
|
@@ -237,6 +338,26 @@ export function latest_manifest_mtime(project_root: string): number {
|
|
|
237
338
|
return mtimes.length > 0 ? Math.max(...mtimes) : 0.0;
|
|
238
339
|
}
|
|
239
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Files whose mtime invalidates a cached detection.
|
|
343
|
+
*
|
|
344
|
+
* The two manifests are not sufficient: the signal table also reads marker
|
|
345
|
+
* files, so adding `components.json` or a `nuxt.config.ts` changes the detected
|
|
346
|
+
* axes while leaving both manifests untouched — and the cache would have served
|
|
347
|
+
* the pre-marker answer indefinitely.
|
|
348
|
+
*
|
|
349
|
+
* `components.json` is listed first so a shadcn adoption, the most common of
|
|
350
|
+
* these, is picked up.
|
|
351
|
+
*/
|
|
352
|
+
const _CACHE_KEY_FILES: ReadonlyArray<string> = [
|
|
353
|
+
'composer.json',
|
|
354
|
+
'package.json',
|
|
355
|
+
'components.json',
|
|
356
|
+
'nuxt.config.ts',
|
|
357
|
+
'nuxt.config.js',
|
|
358
|
+
'astro.config.mjs',
|
|
359
|
+
];
|
|
360
|
+
|
|
240
361
|
/**
|
|
241
362
|
* Read a JSON manifest, returning `{}` on any error.
|
|
242
363
|
*
|
|
@@ -298,18 +419,181 @@ function _has_package(
|
|
|
298
419
|
return name in _all_dependencies(manifest, ...sections);
|
|
299
420
|
}
|
|
300
421
|
|
|
422
|
+
/**
|
|
423
|
+
* Signal table, one entry per axis value.
|
|
424
|
+
*
|
|
425
|
+
* `first match wins` applies **per axis only** — collapsing that across axes is
|
|
426
|
+
* the original defect. A Nuxt project matches `meta: nuxt` AND
|
|
427
|
+
* `reactivity: vue`; both are true, and the flat label could hold only one.
|
|
428
|
+
*
|
|
429
|
+
* Order within an axis is specificity-descending: a marker file beats a bare
|
|
430
|
+
* dependency, and a wrapper is listed before what it wraps.
|
|
431
|
+
*/
|
|
432
|
+
const _AXIS_SIGNALS: ReadonlyArray<{
|
|
433
|
+
readonly axis: keyof StackAxes;
|
|
434
|
+
readonly value: string;
|
|
435
|
+
readonly npm?: ReadonlyArray<string>;
|
|
436
|
+
readonly npm_prefix?: ReadonlyArray<string>;
|
|
437
|
+
readonly composer?: ReadonlyArray<string>;
|
|
438
|
+
readonly files?: ReadonlyArray<string>;
|
|
439
|
+
}> = [
|
|
440
|
+
// meta — wrappers, checked before the layer they wrap
|
|
441
|
+
{ axis: 'meta', value: 'nextjs', npm: ['next'] },
|
|
442
|
+
{ axis: 'meta', value: 'nuxt', npm: ['nuxt'], files: ['nuxt.config.ts', 'nuxt.config.js'] },
|
|
443
|
+
{ axis: 'meta', value: 'astro', npm: ['astro'], files: ['astro.config.mjs'] },
|
|
444
|
+
{ axis: 'meta', value: 'remix', npm: ['@remix-run/react'] },
|
|
445
|
+
{ axis: 'meta', value: 'sveltekit', npm: ['@sveltejs/kit'] },
|
|
446
|
+
{ axis: 'meta', value: 'filament', composer: ['filament/filament'] },
|
|
447
|
+
|
|
448
|
+
// reactivity
|
|
449
|
+
{ axis: 'reactivity', value: 'react', npm: ['react'] },
|
|
450
|
+
{ axis: 'reactivity', value: 'vue', npm: ['vue'] },
|
|
451
|
+
{ axis: 'reactivity', value: 'svelte', npm: ['svelte'] },
|
|
452
|
+
{ axis: 'reactivity', value: 'angular', npm: ['@angular/core'] },
|
|
453
|
+
{ axis: 'reactivity', value: 'solid', npm: ['solid-js'] },
|
|
454
|
+
{ axis: 'reactivity', value: 'qwik', npm: ['@builder.io/qwik'] },
|
|
455
|
+
{ axis: 'reactivity', value: 'livewire', composer: ['livewire/livewire'] },
|
|
456
|
+
{ axis: 'reactivity', value: 'alpine', npm: ['alpinejs'] },
|
|
457
|
+
{ axis: 'reactivity', value: 'htmx', npm: ['htmx.org'] },
|
|
458
|
+
|
|
459
|
+
// view
|
|
460
|
+
{ axis: 'view', value: 'svelte-sfc', npm: ['svelte'] },
|
|
461
|
+
{ axis: 'view', value: 'vue-sfc', npm: ['vue'] },
|
|
462
|
+
// Angular templates are HTML with its own directive syntax, not JSX.
|
|
463
|
+
// Listing it under `jsx` was wrong and would have handed a React idiom to
|
|
464
|
+
// an Angular project through the Phase-3 composition.
|
|
465
|
+
{ axis: 'view', value: 'angular-html', npm: ['@angular/core'] },
|
|
466
|
+
{ axis: 'view', value: 'astro', npm: ['astro'] },
|
|
467
|
+
{ axis: 'view', value: 'jsx', npm: ['react'] },
|
|
468
|
+
{ axis: 'view', value: 'blade', composer: ['laravel/framework'] },
|
|
469
|
+
|
|
470
|
+
// component_lib — marker file beats dependency
|
|
471
|
+
{ axis: 'component_lib', value: 'shadcn', npm: ['shadcn-ui', 'shadcn'], files: ['components.json'] },
|
|
472
|
+
{ axis: 'component_lib', value: 'radix', npm_prefix: ['@radix-ui/'] },
|
|
473
|
+
{ axis: 'component_lib', value: 'flux', composer: ['livewire/flux'] },
|
|
474
|
+
{ axis: 'component_lib', value: 'nuxt-ui', npm: ['@nuxt/ui'] },
|
|
475
|
+
{ axis: 'component_lib', value: 'vuetify', npm: ['vuetify'] },
|
|
476
|
+
{ axis: 'component_lib', value: 'mui', npm: ['@mui/material'] },
|
|
477
|
+
|
|
478
|
+
// css
|
|
479
|
+
{ axis: 'css', value: 'tailwind', npm: ['tailwindcss'] },
|
|
480
|
+
{ axis: 'css', value: 'bootstrap', npm: ['bootstrap'] },
|
|
481
|
+
];
|
|
482
|
+
|
|
483
|
+
/** Axes where two signals are a genuine conflict rather than a stack. */
|
|
484
|
+
const _AMBIGUOUS_AXES: ReadonlyArray<keyof StackAxes> = ['reactivity'];
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Client-side SPA frameworks — two of these in one manifest is a real conflict.
|
|
488
|
+
*
|
|
489
|
+
* Everything else on the reactivity axis co-exists legitimately: Alpine and htmx
|
|
490
|
+
* are progressive-enhancement layers, and Livewire is server-driven, so
|
|
491
|
+
* Laravel+Livewire with a React widget is an ordinary stack, not an ambiguity.
|
|
492
|
+
* Treating any two reactivity signals as a conflict refused those projects —
|
|
493
|
+
* caught by the existing blade-wins-over-react precedence test.
|
|
494
|
+
*/
|
|
495
|
+
const _EXCLUSIVE_REACTIVITY: ReadonlySet<string> = new Set([
|
|
496
|
+
'react',
|
|
497
|
+
'vue',
|
|
498
|
+
'svelte',
|
|
499
|
+
'angular',
|
|
500
|
+
'solid',
|
|
501
|
+
'qwik',
|
|
502
|
+
]);
|
|
503
|
+
|
|
504
|
+
/** Resolve every axis independently from the manifests plus marker files. */
|
|
505
|
+
function _detect_axes(
|
|
506
|
+
composer: Manifest,
|
|
507
|
+
pkg: Manifest,
|
|
508
|
+
components_json: boolean,
|
|
509
|
+
project_root: string,
|
|
510
|
+
): StackAxes {
|
|
511
|
+
const npm = _all_dependencies(pkg, ..._PKG_DEP_KEYS);
|
|
512
|
+
const php = _all_dependencies(composer, 'require', 'require-dev');
|
|
513
|
+
const npm_names = Object.keys(npm);
|
|
514
|
+
const out: Record<string, string> = { ..._EMPTY_AXES };
|
|
515
|
+
const decided = new Set<string>();
|
|
516
|
+
|
|
517
|
+
for (const sig of _AXIS_SIGNALS) {
|
|
518
|
+
if (decided.has(sig.axis)) continue;
|
|
519
|
+
const hit =
|
|
520
|
+
(sig.npm ?? []).some((n) => n in npm) ||
|
|
521
|
+
(sig.npm_prefix ?? []).some((p) => npm_names.some((n) => n.startsWith(p))) ||
|
|
522
|
+
(sig.composer ?? []).some((n) => n in php) ||
|
|
523
|
+
(sig.files ?? []).some(
|
|
524
|
+
(f) =>
|
|
525
|
+
(f === 'components.json' && components_json) ||
|
|
526
|
+
_is_file(path.join(project_root, f)),
|
|
527
|
+
);
|
|
528
|
+
if (hit) {
|
|
529
|
+
out[sig.axis] = sig.value;
|
|
530
|
+
decided.add(sig.axis);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// An unresolved axis on a project that HAS manifests is "not recognised" —
|
|
535
|
+
// a different fact from "absent", and conflating them is what made a
|
|
536
|
+
// refusal indistinguishable from a genuinely plain project.
|
|
537
|
+
const has_manifest = npm_names.length > 0 || Object.keys(php).length > 0;
|
|
538
|
+
if (has_manifest) {
|
|
539
|
+
for (const axis of ['view', 'reactivity'] as const) {
|
|
540
|
+
if (!decided.has(axis)) out[axis] = 'unknown';
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return out as unknown as StackAxes;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** Report axes carrying two mutually exclusive signals. */
|
|
547
|
+
function _detect_ambiguity(composer: Manifest, pkg: Manifest): string[] {
|
|
548
|
+
const npm = _all_dependencies(pkg, ..._PKG_DEP_KEYS);
|
|
549
|
+
const php = _all_dependencies(composer, 'require', 'require-dev');
|
|
550
|
+
const found: string[] = [];
|
|
551
|
+
for (const axis of _AMBIGUOUS_AXES) {
|
|
552
|
+
const hits = _AXIS_SIGNALS.filter(
|
|
553
|
+
(sig) =>
|
|
554
|
+
sig.axis === axis &&
|
|
555
|
+
((sig.npm ?? []).some((n) => n in npm) ||
|
|
556
|
+
(sig.composer ?? []).some((n) => n in php)),
|
|
557
|
+
)
|
|
558
|
+
.map((sig) => sig.value)
|
|
559
|
+
.filter((v) => _EXCLUSIVE_REACTIVITY.has(v));
|
|
560
|
+
if (hits.length > 1) {
|
|
561
|
+
found.push(`${axis}: ${hits.join(' + ')}`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return found;
|
|
565
|
+
}
|
|
566
|
+
|
|
301
567
|
/** Workspace directories a monorepo conventionally puts its packages in. */
|
|
302
568
|
const _WORKSPACE_DIRS: ReadonlyArray<string> = ['packages', 'apps', 'services', 'libs'];
|
|
303
569
|
|
|
304
570
|
/**
|
|
305
|
-
*
|
|
571
|
+
* Return the workspace roots that carry a manifest, sorted.
|
|
306
572
|
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
573
|
+
* Reads the declarative `workspaces` field when present and falls back to the
|
|
574
|
+
* conventional directories. Deliberately shallow — one `readdir` per candidate
|
|
575
|
+
* directory, one `stat` per child.
|
|
576
|
+
*
|
|
577
|
+
* Empty means "not a monorepo". Exactly one means the scope is unambiguous and
|
|
578
|
+
* detection can descend into it. More than one is a question for the caller,
|
|
579
|
+
* not a guess for the detector.
|
|
310
580
|
*/
|
|
311
|
-
function
|
|
312
|
-
|
|
581
|
+
function _nested_frontend_roots(project_root: string): string[] {
|
|
582
|
+
const roots: string[] = [];
|
|
583
|
+
const seen = new Set<string>();
|
|
584
|
+
// `workspaces` is the declarative answer when present; the conventional
|
|
585
|
+
// directories are the fallback for repos that do not declare them.
|
|
586
|
+
const root_pkg = _read_json(path.join(project_root, 'package.json'));
|
|
587
|
+
const declared = root_pkg['workspaces'];
|
|
588
|
+
const globs: string[] = Array.isArray(declared)
|
|
589
|
+
? (declared as unknown[]).filter((g): g is string => typeof g === 'string')
|
|
590
|
+
: [];
|
|
591
|
+
const dirs = new Set<string>(_WORKSPACE_DIRS);
|
|
592
|
+
for (const g of globs) {
|
|
593
|
+
const head = g.split('/')[0];
|
|
594
|
+
if (head !== undefined && head !== '' && !head.includes('*')) dirs.add(head);
|
|
595
|
+
}
|
|
596
|
+
for (const dir of dirs) {
|
|
313
597
|
const base = path.join(project_root, dir);
|
|
314
598
|
let entries: string[];
|
|
315
599
|
try {
|
|
@@ -318,15 +602,18 @@ function _has_nested_manifest(project_root: string): boolean {
|
|
|
318
602
|
continue;
|
|
319
603
|
}
|
|
320
604
|
for (const entry of entries) {
|
|
605
|
+
const child = path.join(base, entry);
|
|
606
|
+
if (seen.has(child)) continue;
|
|
321
607
|
if (
|
|
322
|
-
_is_file(path.join(
|
|
323
|
-
_is_file(path.join(
|
|
608
|
+
_is_file(path.join(child, 'package.json')) ||
|
|
609
|
+
_is_file(path.join(child, 'composer.json'))
|
|
324
610
|
) {
|
|
325
|
-
|
|
611
|
+
seen.add(child);
|
|
612
|
+
roots.push(child);
|
|
326
613
|
}
|
|
327
614
|
}
|
|
328
615
|
}
|
|
329
|
-
return
|
|
616
|
+
return roots.sort();
|
|
330
617
|
}
|
|
331
618
|
|
|
332
619
|
/** True when either manifest names a framework we recognise but do not model. */
|
|
@@ -669,6 +669,61 @@ function _validate_ui_design(ui_design: JsonValue): void {
|
|
|
669
669
|
`required state) or null; got ${pyTypeName(ui_design['states'])}`,
|
|
670
670
|
);
|
|
671
671
|
}
|
|
672
|
+
_validate_provided_artifact(ui_design['provided_artifact']);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Reject a malformed `ui_design.provided_artifact` envelope; tolerate absence.
|
|
677
|
+
*
|
|
678
|
+
* The slot is the port case's only carrier, and both gates that consume it
|
|
679
|
+
* (`design`'s honest-refusal halt, `apply`'s coverage ledger) branch on its
|
|
680
|
+
* shape. Shape-checking here rather than in the handlers means a `interactions:
|
|
681
|
+
* "three"` cannot quietly reduce the coverage ledger to zero declared items —
|
|
682
|
+
* the same bypass-by-type that `states: "n/a"` used to achieve against the
|
|
683
|
+
* five-state loop.
|
|
684
|
+
*/
|
|
685
|
+
function _validate_provided_artifact(provided: JsonValue | undefined): void {
|
|
686
|
+
if (provided === undefined || provided === null) {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if (!_isDict(provided)) {
|
|
690
|
+
throw new SchemaError(
|
|
691
|
+
'state.ui_design.provided_artifact must be a JSON object or null; ' +
|
|
692
|
+
`got ${pyTypeName(provided)}`,
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
if ('path' in provided && typeof provided['path'] !== 'string') {
|
|
696
|
+
throw new SchemaError(
|
|
697
|
+
'state.ui_design.provided_artifact.path must be a string when present',
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
if (
|
|
701
|
+
'design_system' in provided &&
|
|
702
|
+
provided['design_system'] !== null &&
|
|
703
|
+
!_isDict(provided['design_system'])
|
|
704
|
+
) {
|
|
705
|
+
throw new SchemaError(
|
|
706
|
+
'state.ui_design.provided_artifact.design_system must be a JSON ' +
|
|
707
|
+
`object or null; got ${pyTypeName(provided['design_system'])}`,
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
if (
|
|
711
|
+
'loss_acknowledged' in provided &&
|
|
712
|
+
typeof provided['loss_acknowledged'] !== 'boolean'
|
|
713
|
+
) {
|
|
714
|
+
throw new SchemaError(
|
|
715
|
+
'state.ui_design.provided_artifact.loss_acknowledged must be a ' +
|
|
716
|
+
'boolean when present',
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
for (const key of ['interactions', 'keyframes', 'assets']) {
|
|
720
|
+
if (key in provided && !Array.isArray(provided[key])) {
|
|
721
|
+
throw new SchemaError(
|
|
722
|
+
`state.ui_design.provided_artifact.${key} must be a list of ` +
|
|
723
|
+
`strings when present; got ${pyTypeName(provided[key])}`,
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
672
727
|
}
|
|
673
728
|
|
|
674
729
|
/**
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createRequire as __acCreateRequire } from 'node:module'; const require = globalThis.require ?? __acCreateRequire(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// src/install/plan.ts
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
6
|
+
import { join, relative, resolve } from "node:path";
|
|
7
|
+
function sha256File(path) {
|
|
8
|
+
try {
|
|
9
|
+
const data = readFileSync(path);
|
|
10
|
+
return createHash("sha256").update(data).digest("hex");
|
|
11
|
+
} catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function fileEntry(path, kind, options) {
|
|
16
|
+
return {
|
|
17
|
+
path,
|
|
18
|
+
kind,
|
|
19
|
+
sha256: options.hashContent ? sha256File(path) : null
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function walkSourceTree(dir) {
|
|
23
|
+
const out = [];
|
|
24
|
+
walkInto(dir, out);
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
function walkInto(dir, out) {
|
|
28
|
+
let entries;
|
|
29
|
+
try {
|
|
30
|
+
entries = readdirSync(dir).sort();
|
|
31
|
+
} catch {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
for (const name of entries) {
|
|
35
|
+
const full = join(dir, name);
|
|
36
|
+
let isDir;
|
|
37
|
+
try {
|
|
38
|
+
isDir = statSync(full).isDirectory();
|
|
39
|
+
} catch {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (isDir) {
|
|
43
|
+
walkInto(full, out);
|
|
44
|
+
} else {
|
|
45
|
+
out.push(full);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function buildInstallPlan(inputs) {
|
|
50
|
+
const filesByTool = {};
|
|
51
|
+
for (const source of inputs.sources) {
|
|
52
|
+
const bucket = filesByTool[source.toolId] ?? [];
|
|
53
|
+
let files = walkSourceTree(source.srcDir);
|
|
54
|
+
if (source.fileFilter !== void 0) {
|
|
55
|
+
files = files.filter(source.fileFilter);
|
|
56
|
+
}
|
|
57
|
+
for (const srcFile of files) {
|
|
58
|
+
const rel = relative(source.srcDir, srcFile);
|
|
59
|
+
const destPath = resolve(source.destDir, rel);
|
|
60
|
+
bucket.push(
|
|
61
|
+
fileEntry(srcFile, source.kind, {
|
|
62
|
+
hashContent: source.kind !== "bridge"
|
|
63
|
+
})
|
|
64
|
+
);
|
|
65
|
+
bucket[bucket.length - 1] = {
|
|
66
|
+
...bucket[bucket.length - 1],
|
|
67
|
+
path: destPath
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
filesByTool[source.toolId] = bucket;
|
|
71
|
+
}
|
|
72
|
+
for (const source of inputs.sources) {
|
|
73
|
+
if (!(source.toolId in filesByTool)) {
|
|
74
|
+
filesByTool[source.toolId] = [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
version: 2,
|
|
79
|
+
target: inputs.target,
|
|
80
|
+
root: inputs.root,
|
|
81
|
+
filesByTool,
|
|
82
|
+
mergedKeysByTool: {},
|
|
83
|
+
policy: inputs.policy
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export {
|
|
88
|
+
sha256File,
|
|
89
|
+
buildInstallPlan
|
|
90
|
+
};
|