@starci/hfs 4.8.0 → 4.9.1
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/CHANGELOG.md +10 -0
- package/emit/operation-schemas.mjs +3 -0
- package/lint/bound-sys.cjs +2 -2
- package/package.json +1 -1
- package/runtime/engine/admission.mjs +1 -1
- package/runtime/engine/config.mjs +2 -2
- package/runtime/engine/db/machine.mjs +2 -3
- package/runtime/engine/invalid-config.mjs +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +22 -14
- package/runtime/knowledge/hfs/rules.yaml +23 -0
- package/runtime/modules/kernel/failure-codes.yaml +10 -0
- package/runtime/scripts/guards/git-policy.mjs +2 -3
- package/runtime/scripts/hfs/rules/pipeline.mjs +5 -5
- package/runtime/scripts/lib/package-at.mjs +5 -6
- package/runtime/scripts/lib/roots.mjs +2 -2
- package/runtime/scripts/work/draw/draw-dna-findings.mjs +23 -12
- package/runtime/scripts/work/draw/draw-dna.mjs +14 -8
- package/scaffold/app.mjs +4 -4
- package/src/main.mjs +3 -3
- package/sync/index.mjs +14 -1
- package/sync/managed.mjs +3 -2
- package/templates/app/codegen-graphql.mjs.partial +4 -4
- package/templates/app/quality-config/sonar-project.properties +4 -2
- package/runtime/engine/canonical-json.mjs +0 -14
- package/runtime/engine/db/ledger-open.mjs +0 -38
- package/runtime/engine/db/ledger-paths.mjs +0 -87
- package/runtime/engine/db/ledger.mjs +0 -1068
- package/runtime/engine/db/provision-authority.mjs +0 -76
- package/runtime/engine/db/provision-operations.mjs +0 -146
- package/runtime/packages/grammar/scripts/build-stamp.mjs +0 -98
- package/runtime/scripts/agent/codex-app-server.mjs +0 -79
- package/runtime/scripts/agent/codex-mirror-source.mjs +0 -34
- package/runtime/scripts/agent/host-agents.mjs +0 -80
- package/runtime/scripts/agent/launch-trust-policy.mjs +0 -53
- package/runtime/scripts/agent/trust-launch.mjs +0 -191
- package/runtime/scripts/agent/trust.mjs +0 -523
- package/runtime/scripts/api/fs/temp-path.mjs +0 -8
- package/runtime/scripts/api/git/diff-names.mjs +0 -8
- package/runtime/scripts/api/git/head-time.mjs +0 -10
- package/runtime/scripts/api/git/index-lock.mjs +0 -78
- package/runtime/scripts/api/git/log.mjs +0 -5
- package/runtime/scripts/api/git/merge-tree.mjs +0 -5
- package/runtime/scripts/api/git/rev-list.mjs +0 -5
- package/runtime/scripts/api/git/rev-parse.mjs +0 -13
- package/runtime/scripts/api/git/status-query.mjs +0 -5
- package/runtime/scripts/api/git/worktree-list-query.mjs +0 -5
- package/runtime/scripts/api/node/spawn-node.mjs +0 -6
- package/runtime/scripts/api/npm/dist-tag.mjs +0 -8
- package/runtime/scripts/api/npm/lib.mjs +0 -32
- package/runtime/scripts/api/npm/pack-dry-run.mjs +0 -9
- package/runtime/scripts/api/npm/run-npm.mjs +0 -6
- package/runtime/scripts/api/npm/view.mjs +0 -30
- package/runtime/scripts/api/npm/whoami.mjs +0 -8
- package/runtime/scripts/api/process/hide-child-windows.mjs +0 -31
- package/runtime/scripts/api/process/kill-tree.mjs +0 -22
- package/runtime/scripts/api/process/lib.mjs +0 -125
- package/runtime/scripts/api/process/probe-guard-command.mjs +0 -74
- package/runtime/scripts/api/process/process-list.mjs +0 -28
- package/runtime/scripts/api/process/system-tool.mjs +0 -37
- package/runtime/scripts/gates/canon-digest.mjs +0 -81
- package/runtime/scripts/gates/canon-pins.mjs +0 -119
- package/runtime/scripts/gates/gate-input.mjs +0 -98
- package/runtime/scripts/gates/gate.mjs +0 -636
- package/runtime/scripts/gates/grammar-dist.mjs +0 -206
- package/runtime/scripts/gates/output.mjs +0 -40
- package/runtime/scripts/gates/runtime-package-selection.mjs +0 -27
- package/runtime/scripts/gates/test-world-run.mjs +0 -252
- package/runtime/scripts/gates/type-impact.mjs +0 -277
- package/runtime/scripts/gates/type-program.mjs +0 -113
- package/runtime/scripts/guards/assigned-command.mjs +0 -12
- package/runtime/scripts/guards/call-rights.mjs +0 -47
- package/runtime/scripts/guards/command-guard.mjs +0 -468
- package/runtime/scripts/guards/command-policy.mjs +0 -342
- package/runtime/scripts/guards/critic-reach.mjs +0 -82
- package/runtime/scripts/guards/deps-guard.mjs +0 -150
- package/runtime/scripts/guards/env-dump-verdict.mjs +0 -89
- package/runtime/scripts/guards/guards-root.mjs +0 -27
- package/runtime/scripts/guards/hook-install.mjs +0 -343
- package/runtime/scripts/guards/hook-verdict.mjs +0 -12
- package/runtime/scripts/guards/install-verdict.mjs +0 -41
- package/runtime/scripts/guards/kernel-seat.mjs +0 -63
- package/runtime/scripts/guards/launch-verdict.mjs +0 -74
- package/runtime/scripts/guards/op-starci.mjs +0 -16
- package/runtime/scripts/guards/op-write-scope.mjs +0 -27
- package/runtime/scripts/guards/orca-self-lifecycle.mjs +0 -39
- package/runtime/scripts/guards/process-kill-verdict.mjs +0 -106
- package/runtime/scripts/guards/protected-zone.mjs +0 -56
- package/runtime/scripts/guards/refusals.mjs +0 -27
- package/runtime/scripts/guards/rights.mjs +0 -268
- package/runtime/scripts/guards/seat-tools.mjs +0 -47
- package/runtime/scripts/guards/shell-commands.mjs +0 -328
- package/runtime/scripts/guards/supervisor-seat.mjs +0 -80
- package/runtime/scripts/guards/verdict-process.mjs +0 -81
- package/runtime/scripts/lib/app-data.mjs +0 -11
- package/runtime/scripts/lib/canon-binding-text.mjs +0 -129
- package/runtime/scripts/lib/check-format.mjs +0 -7
- package/runtime/scripts/lib/failure-code-reads.mjs +0 -100
- package/runtime/scripts/lib/failure-code-template-errors.mjs +0 -161
- package/runtime/scripts/lib/git-dir.mjs +0 -120
- package/runtime/scripts/lib/guard-command.mjs +0 -24
- package/runtime/scripts/lib/interrupt-cleanup.mjs +0 -38
- package/runtime/scripts/lib/json-formats.mjs +0 -34
- package/runtime/scripts/lib/managed-json.mjs +0 -51
- package/runtime/scripts/lib/menu-parts.mjs +0 -11
- package/runtime/scripts/lib/op-gate.mjs +0 -15
- package/runtime/scripts/lib/provision-control-failure.mjs +0 -32
- package/runtime/scripts/lib/release-version.mjs +0 -134
- package/runtime/scripts/lib/runtime-location.mjs +0 -21
- package/runtime/scripts/lib/runtime-package-inventory.mjs +0 -21
- package/runtime/scripts/lib/runtime-package-manifests.mjs +0 -84
- package/runtime/scripts/lib/runtime-publication-tags.mjs +0 -56
- package/runtime/scripts/lib/seat-env.mjs +0 -43
- package/runtime/scripts/lib/sonar-new-code.mjs +0 -58
- package/runtime/scripts/lib/source-package-imports.mjs +0 -111
- package/runtime/scripts/lib/tar-files.mjs +0 -39
- package/runtime/scripts/lib/uat-run-folder.mjs +0 -118
- package/runtime/scripts/lib/worktree-record.mjs +0 -14
- package/runtime/scripts/machine/ask-receipts.mjs +0 -113
- package/runtime/scripts/machine/evidence-store.mjs +0 -223
- package/runtime/scripts/machine/home.mjs +0 -216
- package/runtime/scripts/machine/host-lock.mjs +0 -184
- package/runtime/scripts/machine/lock-recovery.mjs +0 -55
- package/runtime/scripts/machine/roles-contract.mjs +0 -90
- package/runtime/scripts/machine/runtime-change.mjs +0 -20
- package/runtime/scripts/machine/workflow-tree.mjs +0 -122
- package/runtime/scripts/reconciler/checklist-items.mjs +0 -7
- package/runtime/scripts/reconciler/guard-row.mjs +0 -25
- package/runtime/scripts/work/brand/brand-colour.mjs +0 -125
- package/runtime/scripts/work/brand/brand-tokens.mjs +0 -167
- package/runtime/scripts/work/brand/brand-value-source.mjs +0 -106
- package/runtime/scripts/work/brand/brand.mjs +0 -892
- package/runtime/scripts/work/draw/draw-layer.mjs +0 -306
- package/runtime/scripts/work/draw/draw-rationale-page.mjs +0 -237
- package/runtime/scripts/work/draw/draw-rationale-ref.mjs +0 -14
- package/runtime/scripts/work/draw/draw-rationale.mjs +0 -353
- package/runtime/scripts/work/draw/draw-source.mjs +0 -412
- package/runtime/scripts/work/draw/draw-taste.mjs +0 -278
- package/runtime/scripts/work/draw/draw-typecheck.mjs +0 -72
- package/runtime/scripts/work/draw/rationale-findings.mjs +0 -205
- package/runtime/scripts/work/draw-grammar.mjs +0 -175
- package/runtime/scripts/work/draw-render-page.mjs +0 -188
- package/runtime/scripts/work/draw-render.mjs +0 -626
- package/runtime/scripts/work/png.mjs +0 -237
- package/runtime/scripts/work/render-tools.mjs +0 -64
|
@@ -1,412 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// draw-source.mjs — owner ruling 2026-09-27 ("locked"): interface.draw DRAWS WITH THE REAL GRAMMAR COMPONENTS. The
|
|
3
|
-
// drawer writes each shape as a React `XBase` in `<XBase>.draw.tsx` (the drawing law: the XBase takes {state, props,
|
|
4
|
-
// on} of atoms) composing ONLY @starci/grammar components with their real props and
|
|
5
|
-
// variants, plus fixture JSON per state (and optionally per viewport); scripts/work/draw-render.mjs --component
|
|
6
|
-
// renders it with the product's own CSS. This file is the source gate that runs BEFORE the render:
|
|
7
|
-
//
|
|
8
|
-
// DRAW_TYPECHECK_FAILED TypeScript is the first gate. The draw file is type-checked (the product's own
|
|
9
|
-
// typescript and tsconfig, with @starci/grammar mapped to the grammar the draw
|
|
10
|
-
// renders against - scripts/work/draw-grammar.mjs) - a prop or a variant the
|
|
11
|
-
// grammar does not publish, a missing required prop, a wrong closed value fails.
|
|
12
|
-
// A check that cannot run (no typescript, no grammar types) fails too.
|
|
13
|
-
// DRAW_OFF_GRAMMAR_COMPONENT (AST) a JSX element that is neither a @starci/grammar export, a component
|
|
14
|
-
// declared in the same file (itself walked), a React Fragment nor an allowed
|
|
15
|
-
// layout element; an icon module used as an element (an icon is a `source` prop of
|
|
16
|
-
// Icon/IconTile/IconButton, never rendered raw).
|
|
17
|
-
// DRAW_RAW_STYLED_HTML (AST) raw HTML imitating a component: an HTML element outside the layout set
|
|
18
|
-
// (div, section, article, header, footer, main, aside, nav, ul, ol, li), a `style`
|
|
19
|
-
// or dangerouslySetInnerHTML, a className that is not a string literal, or a class
|
|
20
|
-
// that paints (bg-*, text-*, font-*, rounded-*, border*, shadow*, ring* ...) rather
|
|
21
|
-
// than lays out (flex/grid/gap/padding/order/span/position ...). The same class law
|
|
22
|
-
// applies to a className handed to a grammar component (PageContainer,
|
|
23
|
-
// DescriptionList, MediaFrame ... accept one for placement only).
|
|
24
|
-
// DRAW_IMPORT_OFF_GRAMMAR (AST) a value import from anything but @starci/grammar[/<family>], react and the
|
|
25
|
-
// icon set, the heroicons family on its two cuts (@heroicons/react/24/outline,
|
|
26
|
-
// /16/solid and the matching @starci/heroicons subpaths: knowledge/hfs/fe-stack.yaml
|
|
27
|
-
// icons). A type-only import is free.
|
|
28
|
-
// DRAW_LAYOUT_VALUE_UNJUSTIFIED (AST) a free spacing value - a Tailwind gap/padding/margin/space step outside the
|
|
29
|
-
// grammar GAP/PADDING scale (0 1 2 3 4 6 8: knowledge/ui/presentation gap.yaml,
|
|
30
|
-
// padding.yaml) or an arbitrary [..] value - that no rationale.json decision of kind
|
|
31
|
-
// spacing|layout names with a rule id (the rationale evidence of lane
|
|
32
|
-
// draw-devin-rationale).
|
|
33
|
-
// DRAW_BASE_SIGNATURE the file exports no `<X>Base`, or a fixture is not {state, props, on} (the
|
|
34
|
-
// drawing law: every XBase takes exactly those three).
|
|
35
|
-
//
|
|
36
|
-
// Rendered-DOM ownership (draw-render.mjs measures it, record `ownership`): every element that paints must sit in a
|
|
37
|
-
// grammar component (data-component, a data-grammar-* hook or a starci-core-* class nearer than any drawn layout
|
|
38
|
-
// element) - reported as DRAW_OFF_GRAMMAR_COMPONENT against the rendered DOM.
|
|
39
|
-
//
|
|
40
|
-
// starci work draw-source <X.draw.tsx> [--fixture <json>]... [--product <app dir>]
|
|
41
|
-
// [--grammar auto|product|claude-dist] [--grammar-dist <package root>] [--rationale <file>] [--json]
|
|
42
|
-
import fs from 'node:fs';
|
|
43
|
-
import path from 'node:path';
|
|
44
|
-
import { fileURLToPath } from 'node:url';
|
|
45
|
-
import { list, byCodeUnit } from '../../lib/list.mjs';
|
|
46
|
-
import { isFile } from '../../lib/fs-kind.mjs';
|
|
47
|
-
import { DRAW_OFF_GRAMMAR_COMPONENT } from './draw-dna.mjs';
|
|
48
|
-
import { isMain } from '../../lib/is-main.mjs';
|
|
49
|
-
import { GRAMMAR_PACKAGE, loadTypescript } from './draw-typecheck.mjs';
|
|
50
|
-
|
|
51
|
-
export { DRAW_OFF_GRAMMAR_COMPONENT };
|
|
52
|
-
// The typecheck half lives in draw-typecheck.mjs so draw-grammar.mjs can use it without importing this entry module.
|
|
53
|
-
export { GRAMMAR_PACKAGE, loadTypescript };
|
|
54
|
-
export { typecheckDraw } from './draw-typecheck.mjs';
|
|
55
|
-
export const DRAW_TYPECHECK_FAILED = 'DRAW_TYPECHECK_FAILED';
|
|
56
|
-
export const DRAW_RAW_STYLED_HTML = 'DRAW_RAW_STYLED_HTML';
|
|
57
|
-
export const DRAW_IMPORT_OFF_GRAMMAR = 'DRAW_IMPORT_OFF_GRAMMAR';
|
|
58
|
-
export const DRAW_LAYOUT_VALUE_UNJUSTIFIED = 'DRAW_LAYOUT_VALUE_UNJUSTIFIED';
|
|
59
|
-
export const DRAW_BASE_SIGNATURE = 'DRAW_BASE_SIGNATURE';
|
|
60
|
-
|
|
61
|
-
export const DRAW_SOURCE_SUFFIX = '.draw.tsx';
|
|
62
|
-
const GRAMMAR_MODULE_RX = /^@starci\/grammar(?:\/[a-z][a-z-]*)?$/;
|
|
63
|
-
const ICON_MODULE_RX = /^@(?:heroicons\/react|starci\/heroicons)\/(?:24\/outline|16\/solid)$/;
|
|
64
|
-
const REACT_MODULES = Object.freeze(['react']);
|
|
65
|
-
/** A relative raster import: the art-slot placeholder of a MediaFrame/Image (data-asset-slot; interface.asset owes the art). */
|
|
66
|
-
const ASSET_MODULE_RX = /^\.{1,2}\/[^?#]+\.(?:png|jpe?g|webp|gif|avif)$/i;
|
|
67
|
-
/** HTML a drawing may use, for layout only (grid, region order, list semantics). */
|
|
68
|
-
const LAYOUT_ELEMENTS = Object.freeze(['div', 'section', 'article', 'header', 'footer', 'main', 'aside', 'nav', 'ul', 'ol', 'li']);
|
|
69
|
-
/** The attribute draw-render stamps on every layout element of a draw file (rendered-DOM ownership). */
|
|
70
|
-
export const LAYOUT_ATTR = 'data-draw-layout';
|
|
71
|
-
/** Tailwind steps the grammar spacing scale publishes (GAP-0..6 / PADDING-0..6: 0 .25 .5 .75 1 1.5 2 rem). */
|
|
72
|
-
const GRAMMAR_SPACING_STEPS = Object.freeze(['0', '1', '2', '3', '4', '6', '8']);
|
|
73
|
-
const SPACING_RULE_OF_STEP = Object.freeze({ 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 6: 5, 8: 6 });
|
|
74
|
-
|
|
75
|
-
const RESPONSIVE = /^(?:(?:sm|md|lg|xl|2xl|max-sm|max-md|max-lg|max-xl|@[a-z0-9]+|motion-reduce|print):)*/;
|
|
76
|
-
const SPACING_RX = Object.freeze([
|
|
77
|
-
/^-?gap(?:-[xy])?-(.+)$/,
|
|
78
|
-
/^-?p[xytblrse]?-(.+)$/,
|
|
79
|
-
/^-?m[xytblrse]?-(.+)$/,
|
|
80
|
-
/^-?space-[xy]-(.+)$/,
|
|
81
|
-
/^-?inset(?:-[xy])?-(.+)$/,
|
|
82
|
-
/^-?(?:top|right|bottom|left|start|end)-(.+)$/,
|
|
83
|
-
]);
|
|
84
|
-
/** Pure layout utilities (no paint): display, flex/grid structure, placement, position, sizing to the track. */
|
|
85
|
-
const LAYOUT_CLASS_RX = [
|
|
86
|
-
/^(?:flex|inline-flex|grid|inline-grid|block|inline-block|hidden|contents|isolate|sr-only|not-sr-only)$/,
|
|
87
|
-
/^flex-(?:row|col|row-reverse|col-reverse|wrap|wrap-reverse|nowrap|1|auto|initial|none)$/,
|
|
88
|
-
/^(?:grow|grow-0|shrink|shrink-0)$/, /^basis-(?:\d+\/\d+|full|auto|0)$/,
|
|
89
|
-
/^grid-(?:cols|rows)-(?:\d+|none|subgrid|\[[^\]]+\])$/, /^grid-flow-(?:row|col|dense|row-dense|col-dense)$/,
|
|
90
|
-
/^(?:col|row)-(?:span-(?:\d+|full)|start-\d+|end-\d+|auto)$/, /^order-(?:\d+|first|last|none)$/,
|
|
91
|
-
/^(?:items|justify|content|self|place-items|place-content|place-self|justify-items|justify-self)-[a-z-]+$/,
|
|
92
|
-
/^(?:relative|absolute|static|sticky)$/, /^z-(?:\d+|auto)$/,
|
|
93
|
-
/^(?:w|h|size)-(?:full|auto|0|min|max|fit|screen|dvh|svh|\d+\/\d+|prose|none)$/,
|
|
94
|
-
/^(?:min|max)-[wh]-(?:full|auto|0|min|max|fit|screen|dvh|svh|\d+\/\d+|prose|none)$/,
|
|
95
|
-
/^max-w-(?:xs|sm|md|lg|xl|[2-7]xl)$/, /^overflow-(?:hidden|visible|clip|auto)$/, /^(?:mx|my|m|ms|me)-auto$/,
|
|
96
|
-
];
|
|
97
|
-
|
|
98
|
-
const SKILL_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
|
|
99
|
-
|
|
100
|
-
/** How a spacing class with `step` reads: {kind: layout|token|free, rule?}. */
|
|
101
|
-
function classifySpacing(bare, step) {
|
|
102
|
-
if (step === 'auto' || step === 'full' || (step === 'px' && /^-?(?:top|right|bottom|left|inset)/.test(bare))) return { kind: 'layout' };
|
|
103
|
-
if (!GRAMMAR_SPACING_STEPS.includes(step)) return { kind: 'free' };
|
|
104
|
-
let family = 'INSET';
|
|
105
|
-
if (/^-?(?:gap|space)/.test(bare)) family = 'GAP';
|
|
106
|
-
else if (/^-?p/.test(bare)) family = 'PADDING';
|
|
107
|
-
else if (/^-?m/.test(bare)) family = 'MARGIN';
|
|
108
|
-
return { kind: 'token', rule: family === 'GAP' || family === 'PADDING' ? `${family}-${SPACING_RULE_OF_STEP[step]}` : null };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** How one class reads: {kind: layout|token|free|paint, rule?}. */
|
|
112
|
-
function classifyClass(token) {
|
|
113
|
-
const bare = String(token).replace(RESPONSIVE, '').replace(/^!/, '');
|
|
114
|
-
let spacing = null;
|
|
115
|
-
for (const pattern of SPACING_RX) {
|
|
116
|
-
spacing = pattern.exec(bare);
|
|
117
|
-
if (spacing) break;
|
|
118
|
-
}
|
|
119
|
-
if (spacing) return classifySpacing(bare, spacing[1]);
|
|
120
|
-
if (LAYOUT_CLASS_RX.some((rx) => rx.test(bare))) return { kind: 'layout' };
|
|
121
|
-
return { kind: 'paint' };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** The rationale decision justifying a free spacing class: a spacing|layout decision naming the class, with a rule id. */
|
|
125
|
-
function justifiedBy(cls, rationale) {
|
|
126
|
-
const escaped = cls.replace(/[.*+?^${}()|[\]\\/]/g, String.raw`\$&`);
|
|
127
|
-
const rx = new RegExp(String.raw`(^|[^\w-])${escaped}($|[^\w-])`);
|
|
128
|
-
return list(rationale).find((e) => ['spacing', 'layout'].includes(e?.kind) && list(e.rules).some((r) => String(r).trim())
|
|
129
|
-
&& rx.test(`${e.value ?? ''} ${e.decision ?? ''}`)) ?? null;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
/** The rationale.json beside a draw file: <stem>.rationale.json, <XBase>.rationale.json, else rationale.json. */
|
|
134
|
-
export function rationaleFileFor(file) {
|
|
135
|
-
const dir = path.dirname(file), base = path.basename(file);
|
|
136
|
-
const stems = [base.replace(/\.tsx$/i, ''), base.replace(/\.draw\.tsx$/i, '')];
|
|
137
|
-
return [...stems.map((s) => path.join(dir, `${s}.rationale.json`)), path.join(dir, 'rationale.json')].find((p) => isFile(p)) ?? null;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function loadRationaleEntries(file) {
|
|
141
|
-
if (!file) return [];
|
|
142
|
-
try {
|
|
143
|
-
const doc = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
144
|
-
return Array.isArray(doc) ? doc : list(doc?.entries ?? doc?.decisions);
|
|
145
|
-
} catch { return []; }
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function importedNamesOf(clause, ts, mod, namespaces) {
|
|
149
|
-
const names = [];
|
|
150
|
-
if (clause.name) names.push(clause.name.text);
|
|
151
|
-
const bindings = clause.namedBindings;
|
|
152
|
-
if (bindings && ts.isNamespaceImport(bindings)) {
|
|
153
|
-
names.push(bindings.name.text);
|
|
154
|
-
if (GRAMMAR_MODULE_RX.test(mod)) namespaces.add(bindings.name.text);
|
|
155
|
-
}
|
|
156
|
-
if (bindings && ts.isNamedImports(bindings)) {
|
|
157
|
-
for (const element of bindings.elements) if (!element.isTypeOnly) names.push(element.name.text);
|
|
158
|
-
}
|
|
159
|
-
return { names, bindings };
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function recordDrawImport(mod, names, clause, bindings, ctx) {
|
|
163
|
-
if (!names.length) return;
|
|
164
|
-
if (GRAMMAR_MODULE_RX.test(mod)) for (const name of names) ctx.grammarImports.set(name, mod);
|
|
165
|
-
else if (REACT_MODULES.includes(mod)) for (const name of names) ctx.reactNames.add(name);
|
|
166
|
-
else if (ICON_MODULE_RX.test(mod)) for (const name of names) ctx.iconImports.add(name);
|
|
167
|
-
else if (ASSET_MODULE_RX.test(mod) && clause.name && !bindings) ctx.assetImports.add(clause.name.text);
|
|
168
|
-
else ctx.add(DRAW_IMPORT_OFF_GRAMMAR, ctx.statement, `imports ${names.join(', ')} from ${mod}: a drawing composes only ${GRAMMAR_PACKAGE} (icons from an icon module as a source prop, art as a relative raster import)`);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function inspectDrawImport(statement, ts, ctx) {
|
|
172
|
-
if (!ts.isImportDeclaration(statement)) return;
|
|
173
|
-
const mod = statement.moduleSpecifier.text;
|
|
174
|
-
const clause = statement.importClause;
|
|
175
|
-
if (!clause) {
|
|
176
|
-
ctx.add(DRAW_IMPORT_OFF_GRAMMAR, statement, `imports ${mod} for its side effects: a drawing styles nothing itself (the product CSS is draw-render's --css)`);
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
if (clause.isTypeOnly) return;
|
|
180
|
-
const { names, bindings } = importedNamesOf(clause, ts, mod, ctx.namespaces);
|
|
181
|
-
recordDrawImport(mod, names, clause, bindings, { ...ctx, statement });
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function collectDrawImports(sf, ts, add) {
|
|
185
|
-
const context = { add, grammarImports: new Map(), namespaces: new Set(), iconImports: new Set(), assetImports: new Set(), reactNames: new Set() };
|
|
186
|
-
for (const statement of sf.statements) inspectDrawImport(statement, ts, context);
|
|
187
|
-
return context;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function drawIsExported(node, ts) {
|
|
191
|
-
return list(ts.getModifiers?.(node) ?? node.modifiers).some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function inspectFunctionDeclaration(statement, ts, context) {
|
|
195
|
-
if (ts.isFunctionDeclaration(statement) && statement.name) {
|
|
196
|
-
if (/^[A-Z]/.test(statement.name.text)) context.local.add(statement.name.text);
|
|
197
|
-
if (drawIsExported(statement, ts)) context.exported.push(statement.name.text);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function inspectVariableDeclaration(statement, ts, context) {
|
|
202
|
-
if (!ts.isVariableStatement(statement)) return;
|
|
203
|
-
for (const declaration of statement.declarationList.declarations) {
|
|
204
|
-
if (!ts.isIdentifier(declaration.name)) continue;
|
|
205
|
-
if (/^[A-Z]/.test(declaration.name.text) && declaration.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) context.local.add(declaration.name.text);
|
|
206
|
-
if (drawIsExported(statement, ts)) context.exported.push(declaration.name.text);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function inspectExportDeclaration(statement, ts, context) {
|
|
211
|
-
if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
|
|
212
|
-
for (const element of statement.exportClause.elements) context.exported.push(element.name.text);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function collectDrawDeclarations(sf, ts) {
|
|
217
|
-
const context = { local: new Set(), exported: [] };
|
|
218
|
-
for (const statement of sf.statements) {
|
|
219
|
-
inspectFunctionDeclaration(statement, ts, context);
|
|
220
|
-
inspectVariableDeclaration(statement, ts, context);
|
|
221
|
-
inspectExportDeclaration(statement, ts, context);
|
|
222
|
-
}
|
|
223
|
-
return context;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function checkDrawClassName(attr, owner, ctx) {
|
|
227
|
-
const init = attr.initializer;
|
|
228
|
-
let value = null;
|
|
229
|
-
if (init && ctx.ts.isStringLiteral(init)) value = init.text;
|
|
230
|
-
else if (init && ctx.ts.isJsxExpression(init) && init.expression && (ctx.ts.isStringLiteral(init.expression) || ctx.ts.isNoSubstitutionTemplateLiteral(init.expression))) value = init.expression.text;
|
|
231
|
-
if (value == null) {
|
|
232
|
-
ctx.add(DRAW_RAW_STYLED_HTML, attr, `<${owner}> className is not a string literal: a drawing's classes must be readable to the gate`);
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
for (const token of value.split(/\s+/).filter(Boolean)) {
|
|
236
|
-
const classification = classifyClass(token);
|
|
237
|
-
if (classification.kind === 'paint') {
|
|
238
|
-
ctx.classes.paint.push(token);
|
|
239
|
-
ctx.add(DRAW_RAW_STYLED_HTML, attr, `<${owner} className="${token}"> paints (colour, type, radius, border or shadow) or is not a layout utility: that is a grammar component's job, never raw HTML`);
|
|
240
|
-
} else if (classification.kind === 'free') {
|
|
241
|
-
ctx.classes.free.push(token);
|
|
242
|
-
if (!justifiedBy(token, ctx.rationale)) ctx.add(DRAW_LAYOUT_VALUE_UNJUSTIFIED, attr, `<${owner} className="${token}"> is a free spacing value outside the grammar GAP/PADDING scale (${GRAMMAR_SPACING_STEPS.join(' ')}): justify it in rationale.json (a spacing|layout decision naming ${token} with its rule id) or use a grammar step`);
|
|
243
|
-
} else if (classification.kind === 'token') ctx.classes.token.push(token);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function drawTagName(tag, ts, sf) {
|
|
248
|
-
if (ts.isIdentifier(tag)) return tag.text;
|
|
249
|
-
if (ts.isPropertyAccessExpression(tag)) return `${tag.expression.getText(sf)}.${tag.name.text}`;
|
|
250
|
-
return tag.getText(sf);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function inspectDrawTag(node, tagNode, ctx) {
|
|
254
|
-
const tag = drawTagName(tagNode, ctx.ts, ctx.sf);
|
|
255
|
-
const intrinsic = ctx.ts.isIdentifier(tagNode) && /^[a-z]/.test(tag);
|
|
256
|
-
if (intrinsic) {
|
|
257
|
-
if (!LAYOUT_ELEMENTS.includes(tag)) ctx.add(DRAW_RAW_STYLED_HTML, node, `<${tag}> is raw HTML: text is Text/Heading, media is Image/MediaFrame, icons are Icon - HTML is for layout only (${LAYOUT_ELEMENTS.join(', ')})`);
|
|
258
|
-
else ctx.layout.push({ tag, line: ctx.lineOf(node) });
|
|
259
|
-
} else if (ctx.ts.isPropertyAccessExpression(tagNode)) {
|
|
260
|
-
const root = tagNode.expression.getText(ctx.sf);
|
|
261
|
-
if (!ctx.namespaces.has(root) && !(ctx.reactNames.has(root) && tagNode.name.text === 'Fragment')) ctx.add(DRAW_OFF_GRAMMAR_COMPONENT, node, `<${tag}> is not a ${GRAMMAR_PACKAGE} export`);
|
|
262
|
-
} else if (ctx.iconImports.has(tag)) {
|
|
263
|
-
ctx.add(DRAW_OFF_GRAMMAR_COMPONENT, node, `<${tag}> renders an icon module raw: pass it as the source of Icon, IconTile or IconButton`);
|
|
264
|
-
} else if (!ctx.grammarImports.has(tag) && !ctx.local.has(tag) && !(ctx.reactNames.has(tag) && tag === 'Fragment')) {
|
|
265
|
-
ctx.add(DRAW_OFF_GRAMMAR_COMPONENT, node, `<${tag}> is neither a ${GRAMMAR_PACKAGE} export nor a component of this file`);
|
|
266
|
-
}
|
|
267
|
-
return tag;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function inspectDrawAttributes(attributes, tag, ctx) {
|
|
271
|
-
for (const attr of attributes.properties) {
|
|
272
|
-
if (!ctx.ts.isJsxAttribute(attr)) continue;
|
|
273
|
-
const name = attr.name.getText(ctx.sf);
|
|
274
|
-
if (name === 'style') ctx.add(DRAW_RAW_STYLED_HTML, attr, `<${tag} style> hand-styles: layout uses classes on the grammar spacing scale, paint belongs to grammar components`);
|
|
275
|
-
else if (name === 'dangerouslySetInnerHTML') ctx.add(DRAW_RAW_STYLED_HTML, attr, `<${tag} dangerouslySetInnerHTML> injects raw markup`);
|
|
276
|
-
else if (name === 'className') checkDrawClassName(attr, tag, ctx);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function visitDrawElement(node, tagNode, attributes, ctx) {
|
|
281
|
-
const tag = inspectDrawTag(node, tagNode, ctx);
|
|
282
|
-
inspectDrawAttributes(attributes, tag, ctx);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function walkDrawSource(node, ctx) {
|
|
286
|
-
if (ctx.ts.isJsxElement(node)) visitDrawElement(node.openingElement, node.openingElement.tagName, node.openingElement.attributes, ctx);
|
|
287
|
-
else if (ctx.ts.isJsxSelfClosingElement(node)) visitDrawElement(node, node.tagName, node.attributes, ctx);
|
|
288
|
-
ctx.ts.forEachChild(node, (child) => { walkDrawSource(child, ctx); });
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* The AST gate of one draw file's text. Returns {findings:[{code, detail, line}], exports:[names], grammarImports,
|
|
293
|
-
* layout:[{tag, line}], classes:{token, free, paint}}.
|
|
294
|
-
*/
|
|
295
|
-
export function sourceFindings(text, { file = 'source.draw.tsx', rationale = [], ts = loadTypescript()?.ts } = {}) {
|
|
296
|
-
if (!ts) return { findings: [{ code: DRAW_TYPECHECK_FAILED, detail: `${path.basename(file)}: no typescript install to parse the draw source` }], exports: [], grammarImports: new Map(), layout: [], classes: { token: [], free: [], paint: [] } };
|
|
297
|
-
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
298
|
-
const label = path.basename(file);
|
|
299
|
-
const findings = [];
|
|
300
|
-
const lineOf = (node) => sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
301
|
-
const add = (code, node, detail) => {
|
|
302
|
-
const location = node ? `:${lineOf(node)}` : '';
|
|
303
|
-
findings.push({ code, line: node ? lineOf(node) : null, detail: `${label}${location} ${detail}` });
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
// Imports: grammar, react, icon sources, a raster art placeholder; a type-only import is free.
|
|
307
|
-
const { grammarImports, namespaces, iconImports, assetImports, reactNames } = collectDrawImports(sf, ts, add);
|
|
308
|
-
|
|
309
|
-
// Components declared in this file (each is walked like the XBase itself) and the exports.
|
|
310
|
-
const { local, exported } = collectDrawDeclarations(sf, ts);
|
|
311
|
-
if (!exported.some((n) => /^[A-Z]\w*Base$/.test(n))) add(DRAW_BASE_SIGNATURE, null, 'exports no <X>Base: the drawn shape is the XBase of the drawing law');
|
|
312
|
-
|
|
313
|
-
const layout = [];
|
|
314
|
-
const classes = { token: [], free: [], paint: [] };
|
|
315
|
-
const context = { ts, sf, add, lineOf, grammarImports, namespaces, iconImports, reactNames, local, rationale, layout, classes };
|
|
316
|
-
walkDrawSource(sf, context);
|
|
317
|
-
return { findings, exports: exported, grammarImports, layout, classes, assetImports: [...assetImports] };
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/** Findings of the fixtures: each one is {state, props, on} (the drawing law's XBase input). */
|
|
321
|
-
function fixtureFindings(fixtures) {
|
|
322
|
-
const out = [];
|
|
323
|
-
for (const { file, value } of fixtures) {
|
|
324
|
-
const label = path.basename(file);
|
|
325
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) { out.push({ code: DRAW_BASE_SIGNATURE, detail: `${label} is not a props object` }); continue; }
|
|
326
|
-
const keys = Object.keys(value).toSorted(byCodeUnit);
|
|
327
|
-
if (keys.join(',') !== 'on,props,state') out.push({ code: DRAW_BASE_SIGNATURE, detail: `${label} holds {${keys.join(', ')}}: an XBase fixture is exactly {state, props, on} (the drawing law)` });
|
|
328
|
-
else if (typeof value.state !== 'string') out.push({ code: DRAW_BASE_SIGNATURE, detail: `${label} state is not a shape name` });
|
|
329
|
-
}
|
|
330
|
-
return out;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/** Insert `data-draw-layout=""` into every intrinsic element of a draw file, for the rendered-DOM ownership probe. */
|
|
334
|
-
export function markLayoutElements(text, { ts = loadTypescript()?.ts, file = 'source.draw.tsx' } = {}) {
|
|
335
|
-
if (!ts) return text;
|
|
336
|
-
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
337
|
-
const at = [];
|
|
338
|
-
const walk = (node) => {
|
|
339
|
-
let opening = null;
|
|
340
|
-
if (ts.isJsxElement(node)) { opening = node.openingElement; }
|
|
341
|
-
else if (ts.isJsxSelfClosingElement(node)) { opening = node; }
|
|
342
|
-
if (opening && ts.isIdentifier(opening.tagName) && /^[a-z]/.test(opening.tagName.text)) at.push(opening.tagName.getEnd());
|
|
343
|
-
ts.forEachChild(node, walk);
|
|
344
|
-
};
|
|
345
|
-
walk(sf);
|
|
346
|
-
let out = text;
|
|
347
|
-
for (const pos of at.toSorted((a, b) => b - a)) out = `${out.slice(0, pos)} ${LAYOUT_ATTR}=""${out.slice(pos)}`;
|
|
348
|
-
return out;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
export const typecheckFindings = (result, { label }) => {
|
|
353
|
-
if (result.ok) return [];
|
|
354
|
-
const errors = result.errors.slice(0, 6).map((e) => {
|
|
355
|
-
const file = e.file ? path.basename(e.file) : '';
|
|
356
|
-
const line = e.line ? `:${e.line}` : '';
|
|
357
|
-
return `${file}${line} ${e.code ?? ''} ${e.message}`;
|
|
358
|
-
}).join(' | ');
|
|
359
|
-
return [{ code: DRAW_TYPECHECK_FAILED, detail: `${label} does not type-check against the grammar it renders with (${result.errors.length} error(s)): ${errors}` }];
|
|
360
|
-
};
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* The whole source gate of one draw file: grammar resolution (typecheck-driven, draw-grammar.mjs), then the AST gate
|
|
364
|
-
* and the fixtures. Returns {findings, grammar (the resolution record), ast, rationale}.
|
|
365
|
-
*/
|
|
366
|
-
export async function checkDrawSource({ file, fixtures = [], productDir, skillRoot = SKILL_ROOT, grammarDist = null, prefer = 'auto', rationaleFile = undefined, typecheck = undefined }) {
|
|
367
|
-
const { resolveDrawGrammar } = await import('../draw-grammar.mjs');
|
|
368
|
-
const text = fs.readFileSync(file, 'utf8');
|
|
369
|
-
const label = path.basename(file);
|
|
370
|
-
const why = rationaleFile === undefined ? rationaleFileFor(file) : rationaleFile;
|
|
371
|
-
const ts = loadTypescript([productDir])?.ts;
|
|
372
|
-
const ast = sourceFindings(text, { file, rationale: loadRationaleEntries(why), ts });
|
|
373
|
-
const grammar = resolveDrawGrammar({ file, productDir, skillRoot, grammarDist, prefer, ...(typecheck ? { typecheck } : {}) });
|
|
374
|
-
const failed = grammar.attempts.at(-1) ?? { ok: false, errors: [{ message: grammar.error ?? 'no grammar candidate' }] };
|
|
375
|
-
const findings = [...(grammar.ok ? [] : typecheckFindings(failed, { label })),
|
|
376
|
-
...ast.findings.map(({ code, detail }) => ({ code, detail })),
|
|
377
|
-
...fixtureFindings(fixtures.map((f) => ({ file: f, value: (() => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return null; } })() })))];
|
|
378
|
-
return { findings, grammar, ast: { exports: ast.exports, layout: ast.layout, classes: ast.classes, grammarImports: Object.fromEntries(ast.grammarImports ?? []) }, rationale: why };
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/* ------------------------------------------------------------------------ CLI */
|
|
382
|
-
|
|
383
|
-
const VALUE_FLAGS = new Set(['--fixture', '--product', '--grammar', '--grammar-dist', '--rationale']);
|
|
384
|
-
|
|
385
|
-
async function main(argv) {
|
|
386
|
-
const json = argv.includes('--json');
|
|
387
|
-
const file = argv.find((a, i) => !a.startsWith('--') && !VALUE_FLAGS.has(argv[i - 1]));
|
|
388
|
-
const vals = (k) => argv.flatMap((a, i) => (argv[i - 1] === k ? [a] : []));
|
|
389
|
-
if (!file) { process.stderr.write('use: starci work draw-source <X.draw.tsx> [--fixture <json>]... [--product <app dir>] [--grammar auto|product|claude-dist] [--grammar-dist <package root>] [--rationale <file>] [--json]\n'); return 2; }
|
|
390
|
-
const productDir = path.resolve(vals('--product')[0] ?? path.dirname(file));
|
|
391
|
-
const r = await checkDrawSource({ file: path.resolve(file), fixtures: vals('--fixture').map((f) => path.resolve(f)), productDir, prefer: vals('--grammar')[0] ?? 'auto',
|
|
392
|
-
grammarDist: vals('--grammar-dist')[0] ?? null, rationaleFile: vals('--rationale')[0] ? path.resolve(vals('--rationale')[0]) : undefined });
|
|
393
|
-
const out = { ok: r.findings.length === 0, findings: r.findings, grammar: { ok: r.grammar.ok, grammarSource: r.grammar.grammarSource, upgradeOwed: r.grammar.upgradeOwed,
|
|
394
|
-
attempts: r.grammar.attempts.map((a) => ({ source: a.source, version: a.version, ok: a.ok, errors: a.errors.length })) } };
|
|
395
|
-
if (json) process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
|
|
396
|
-
else {
|
|
397
|
-
const upgrade = r.grammar.upgradeOwed ? ` (product upgrade owed ${r.grammar.upgradeOwed.from} -> ${r.grammar.upgradeOwed.to})` : '';
|
|
398
|
-
const findings = r.findings.map((f) => ` [${f.code}] ${f.detail}`).join('\n');
|
|
399
|
-
process.stdout.write(`${out.ok ? 'ok' : 'REFUSED'}: ${path.basename(file)} grammar ${r.grammar.grammarSource ?? 'unresolved'}${upgrade}\n${findings}\n`);
|
|
400
|
-
}
|
|
401
|
-
return out.ok ? 0 : 1;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
if (isMain(import.meta.url)) {
|
|
405
|
-
// Top-level await is safe: draw-grammar.mjs imports draw-typecheck.mjs, not this entry, so no dependency waits on main().
|
|
406
|
-
try {
|
|
407
|
-
process.exitCode = await main(process.argv.slice(2));
|
|
408
|
-
} catch (e) {
|
|
409
|
-
process.stderr.write(`draw-source: ${e?.stack ?? e}\n`);
|
|
410
|
-
process.exitCode = 2;
|
|
411
|
-
}
|
|
412
|
-
}
|