@starci/hfs 4.16.4 → 4.17.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/CHANGELOG.md +35 -3
- package/README.md +4 -4
- package/codemod/loading-states.d.mts +50 -0
- package/codemod/loading-states.mjs +384 -0
- package/codemod/main.d.mts +1 -1
- package/codemod/main.mjs +46 -5
- package/codemod/page-navigation.d.mts +50 -0
- package/codemod/page-navigation.mjs +278 -0
- package/codemod/section-frame.d.mts +50 -0
- package/codemod/section-frame.mjs +483 -0
- package/codemod/small-renames.d.mts +54 -0
- package/codemod/small-renames.mjs +614 -0
- package/codemod/surface-card-kinds.d.mts +49 -0
- package/codemod/surface-card-kinds.mjs +404 -0
- package/codemod/theme-tokens.d.mts +45 -0
- package/codemod/theme-tokens.mjs +114 -0
- package/codemod/tier-binding.d.mts +50 -0
- package/codemod/tier-binding.mjs +214 -0
- package/package.json +1 -1
- package/runtime/knowledge/grammars/core/playbook.yaml +41 -51
- package/runtime/knowledge/grammars/offset-pop/playbook.yaml +5 -5
- package/runtime/knowledge/hfs/canon-pins.yaml +7 -7
- package/runtime/knowledge/hfs/rules.yaml +343 -26
- package/runtime/knowledge/hfs/slots.yaml +12 -5
- package/runtime/knowledge/patterns/fe/folder.yaml +1 -1
- package/runtime/modules/kernel/failure-codes.yaml +23 -13
- package/runtime/scripts/hfs/architecture/clones.mjs +12 -2
- package/runtime/scripts/hfs/architecture/component-tiers.d.mts +46 -0
- package/runtime/scripts/hfs/architecture/component-tiers.mjs +161 -0
- package/runtime/scripts/hfs/architecture/config.mjs +1 -1
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
- package/runtime/scripts/hfs/architecture/frontend.mjs +2 -2
- package/runtime/scripts/hfs/architecture/index.mjs +6 -0
- package/runtime/scripts/hfs/architecture/render-graph.d.mts +125 -0
- package/runtime/scripts/hfs/architecture/render-graph.mjs +449 -0
- package/runtime/scripts/hfs/architecture/render-prop-across-server.d.mts +23 -0
- package/runtime/scripts/hfs/architecture/render-prop-across-server.mjs +58 -0
- package/runtime/scripts/hfs/architecture/surface-nesting.d.mts +44 -0
- package/runtime/scripts/hfs/architecture/surface-nesting.mjs +127 -0
- package/runtime/scripts/hfs/slot-manifest-shape.mjs +16 -1
- package/runtime/scripts/work/draw/draw-dna.mjs +1 -1
- package/scaffold/add-fe-layout.mjs +3 -3
- package/scaffold/add-fe-overlay.mjs +4 -4
- package/scaffold/add-fe-page.mjs +1 -1
- package/scaffold/add-fe-unit.d.mts +2 -1
- package/scaffold/add-fe-unit.mjs +14 -12
- package/scaffold/add.d.mts +1 -1
- package/scaffold/add.mjs +1 -1
- package/src/main.mjs +4 -4
- package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/component.tsx +3 -4
- package/templates/fe/skeleton/apps/app/src/app/[locale]/layout.tsx +8 -5
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/layout.tsx +1 -3
- package/templates/fe/skeleton/packages/__project__-ui/src/branches/Providers/index.tsx +3 -3
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/LocaleShell/index.tsx +3 -5
- package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -1
- package/templates/fe/skeleton-lite/apps/web/src/features/layouts/AppLayout/index.tsx +1 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/brand/brand.css +1 -2
- package/templates/fe/units/block/component.tsx.tpl +2 -3
- package/templates/fe/units/branch/index.tsx.tpl +4 -3
- package/templates/fe/units/composite/index.tsx.tpl +3 -3
- package/templates/fe/units/frame/index.tsx.tpl +10 -0
- package/templates/fe/units/layout/index.tsx.tpl +2 -2
- package/templates/fe/units/leaf/index.tsx.tpl +3 -3
- package/templates/fe/units/page/component.tsx.tpl +2 -2
- package/templates/fe/units/page/index.tsx.tpl +2 -2
- package/templates/fe/units/page/stories.tsx.tpl +2 -2
- package/templates/fe/units/slot/SlotView.tsx.tpl +11 -10
- package/templates/fe/units/slot/data-slot.ts.tpl +20 -8
- package/templates/fe/units/slot/slot.shared.ts.tpl +52 -11
- package/templates/fe/units/slot/useSlotOfReads.ts.tpl +19 -0
- /package/templates/fe/skeleton/packages/__project__-ui/src/{composites → frames}/SiteShell/classNames.ts +0 -0
- /package/templates/fe/skeleton/packages/__project__-ui/src/{composites → frames}/SiteShell/index.tsx +0 -0
- /package/templates/fe/skeleton-lite/apps/web/src/components/{composites → frames}/SiteShell/classNames.ts +0 -0
- /package/templates/fe/skeleton-lite/apps/web/src/components/{composites → frames}/SiteShell/index.tsx +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** The TypeScript compiler API of the app's own install (loadTypescript), typed by the version this package develops against. */
|
|
2
|
+
type Ts = typeof import('typescript');
|
|
3
|
+
/** One safe rewrite of the report. */
|
|
4
|
+
export interface TierBindingChange {
|
|
5
|
+
file: string;
|
|
6
|
+
line: number;
|
|
7
|
+
column: number;
|
|
8
|
+
code: string;
|
|
9
|
+
from: string;
|
|
10
|
+
to: string;
|
|
11
|
+
}
|
|
12
|
+
/** One case left for a person. */
|
|
13
|
+
export interface TierBindingFinding {
|
|
14
|
+
file: string;
|
|
15
|
+
line: number;
|
|
16
|
+
column: number;
|
|
17
|
+
code: string;
|
|
18
|
+
reason: string;
|
|
19
|
+
component?: string;
|
|
20
|
+
source?: string;
|
|
21
|
+
}
|
|
22
|
+
/** The report of a migration (`starci app codemod tier-binding`). */
|
|
23
|
+
export interface TierBindingReport {
|
|
24
|
+
schema: string;
|
|
25
|
+
root: string;
|
|
26
|
+
write: boolean;
|
|
27
|
+
filesScanned: number;
|
|
28
|
+
filesChanged: string[];
|
|
29
|
+
changes: TierBindingChange[];
|
|
30
|
+
findings: TierBindingFinding[];
|
|
31
|
+
}
|
|
32
|
+
/** Migrate nested JSX children to named props and report the cases the types now refuse; a case with no mechanical rewrite keeps its source. */
|
|
33
|
+
export declare function transformTierBinding({ ts, text, file }: {
|
|
34
|
+
ts: Ts;
|
|
35
|
+
text: string;
|
|
36
|
+
file?: string;
|
|
37
|
+
}): {
|
|
38
|
+
text: string;
|
|
39
|
+
changes: TierBindingChange[];
|
|
40
|
+
findings: TierBindingFinding[];
|
|
41
|
+
};
|
|
42
|
+
/** Preview or apply the safe rewrites in one app's FE tree (`fe/`, or `src/` and `stories/` of a single-side app); every unresolved case is kept in the returned report. */
|
|
43
|
+
export declare function migrateTierBinding({ root, write, ts }: {
|
|
44
|
+
root: string;
|
|
45
|
+
write?: boolean;
|
|
46
|
+
ts?: Ts | null;
|
|
47
|
+
}): TierBindingReport;
|
|
48
|
+
/** Render the migration and what is left for a person as Markdown; JSON consumers read the same report object. */
|
|
49
|
+
export declare function tierBindingMarkdown(report: TierBindingReport): string;
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { loadTypescript } from '../runtime/scripts/lib/package-at.mjs';
|
|
5
|
+
import { sourceLocation } from '../runtime/scripts/lib/ts-ast.mjs';
|
|
6
|
+
import { shadowed } from './small-renames.mjs';
|
|
7
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
8
|
+
const GRAMMAR = /^@starci\/grammar(?:\/|$)/;
|
|
9
|
+
const GENERATED = new Set(['node_modules', '.next', '.turbo', 'dist', 'coverage', '.git']);
|
|
10
|
+
/** Components whose child became a named prop: the JSX child moves into it. `text` takes a string, the rest a node. */
|
|
11
|
+
const CHILD_TO_PROP = new Map([
|
|
12
|
+
['Tooltip', { prop: 'trigger' }], ['Disclosure', { prop: 'body' }], ['DragItem', { prop: 'body' }], ['MessageBubble', { prop: 'body' }],
|
|
13
|
+
['MediaFrame', { prop: 'media' }], ['Field', { prop: 'control' }], ['Quote', { prop: 'text', text: true }],
|
|
14
|
+
]);
|
|
15
|
+
/** Leaves whose only child is their text label (`LabelChild`). */
|
|
16
|
+
const LABEL_ONLY = new Set(['Button', 'Badge', 'Heading', 'Link', 'TextAction', 'Code']);
|
|
17
|
+
/** Components that take no children at all (`NoChildren`): their anatomy is fields and named slots. */
|
|
18
|
+
const NO_CHILDREN = new Set([
|
|
19
|
+
'Avatar', 'AvatarButton', 'Checkbox', 'DateField', 'Divider', 'Icon', 'IconButton', 'IconTile', 'Image', 'Kbd', 'Mascot', 'Meter', 'NumberField', 'PressableField',
|
|
20
|
+
'Progress', 'ProgressCircle', 'RelativeTime', 'SearchField', 'Skeleton', 'Slider', 'Spinner', 'StatusDot', 'Switch', 'TimeField', 'Video',
|
|
21
|
+
'Accordion', 'BubbleMap', 'Calendar', 'ComboBox', 'CopyField', 'DataTable', 'DatePicker', 'DateRangePicker', 'DropdownMenu', 'FileDropzone', 'FlowGraph', 'Input', 'ListBox',
|
|
22
|
+
'OtpInput', 'Rating', 'Select', 'Tabs', 'Terminal', 'Textarea',
|
|
23
|
+
'Alert', 'AvatarGroup', 'ButtonGroup', 'CheckboxGroup', 'EmptyNotice', 'FilterButtonGroup', 'Footer', 'IdentityLockup', 'Pagination', 'RadioGroup',
|
|
24
|
+
'SegmentedControl', 'Sidebar', 'Stepper', 'TagGroup', 'Timeline', 'Toaster', 'TopBar',
|
|
25
|
+
]);
|
|
26
|
+
/** Components that lost `className`: the stylesheet and the structure decide the look. `Freestyle` is the one hole and keeps it. */
|
|
27
|
+
const NO_CLASS_NAME = new Set([...NO_CHILDREN, ...LABEL_ONLY, ...CHILD_TO_PROP.keys(), 'Page', 'PaneGrid', 'NavLandmark', 'GrammarRoot', 'HorizontalScrollRegion', 'VerticalScrollRegion', 'WorkspaceShell', 'Stack', 'Cluster', 'Grid']);
|
|
28
|
+
/** The JSX text between two tags as React reads it: lines trimmed and joined by one space, blank lines dropped. */
|
|
29
|
+
function jsxText(raw) {
|
|
30
|
+
return raw.split(/\r?\n/).map(line => line.trim()).filter(Boolean).join(' ');
|
|
31
|
+
}
|
|
32
|
+
/** Migrate nested JSX children to named props and report the cases the types now refuse; a case with no mechanical rewrite keeps its source. */
|
|
33
|
+
export function transformTierBinding({ ts, text, file = 'component.tsx' }) {
|
|
34
|
+
const changes = [];
|
|
35
|
+
const findings = [];
|
|
36
|
+
if (!text.includes('@starci/grammar'))
|
|
37
|
+
return { text, changes, findings };
|
|
38
|
+
const syntax = ts.transpileModule(text, { fileName: file, reportDiagnostics: true, compilerOptions: { jsx: ts.JsxEmit.Preserve, target: ts.ScriptTarget.Latest } }).diagnostics ?? [];
|
|
39
|
+
if (syntax.length)
|
|
40
|
+
return { text, changes, findings: [{ file, line: 1, column: 1, code: 'PARSE_ERROR', reason: ts.flattenDiagnosticMessageText(syntax[0]?.messageText ?? '', '\n') }] };
|
|
41
|
+
let current = text;
|
|
42
|
+
// A child moves into a prop of its parent; the parent's own parent may then be rewritten too, so passes run until nothing changes.
|
|
43
|
+
for (let pass = 0; pass < 12; pass += 1) {
|
|
44
|
+
const result = onePass({ ts, text: current, file });
|
|
45
|
+
if (pass === 0)
|
|
46
|
+
findings.push(...result.findings);
|
|
47
|
+
changes.push(...result.changes);
|
|
48
|
+
if (result.text === current)
|
|
49
|
+
break;
|
|
50
|
+
current = result.text;
|
|
51
|
+
}
|
|
52
|
+
return { text: current, changes, findings };
|
|
53
|
+
}
|
|
54
|
+
function onePass({ ts, text, file }) {
|
|
55
|
+
const changes = [];
|
|
56
|
+
const findings = [];
|
|
57
|
+
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, /\.jsx$/i.test(file) ? ts.ScriptKind.JSX : ts.ScriptKind.TSX);
|
|
58
|
+
const locals = new Map();
|
|
59
|
+
const namespaces = new Set();
|
|
60
|
+
for (const statement of source.statements) {
|
|
61
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || !GRAMMAR.test(statement.moduleSpecifier.text))
|
|
62
|
+
continue;
|
|
63
|
+
const named = statement.importClause?.namedBindings;
|
|
64
|
+
if (named && ts.isNamespaceImport(named))
|
|
65
|
+
namespaces.add(named.name.text);
|
|
66
|
+
if (named && ts.isNamedImports(named))
|
|
67
|
+
for (const specifier of named.elements)
|
|
68
|
+
locals.set(specifier.name.text, specifier.propertyName?.text ?? specifier.name.text);
|
|
69
|
+
}
|
|
70
|
+
if (!locals.size && !namespaces.size)
|
|
71
|
+
return { text, changes, findings };
|
|
72
|
+
const resolve = (tag) => {
|
|
73
|
+
if (ts.isIdentifier(tag)) {
|
|
74
|
+
const imported = locals.get(tag.text);
|
|
75
|
+
return imported !== undefined && !shadowed(ts, tag) ? { name: imported } : null;
|
|
76
|
+
}
|
|
77
|
+
if (ts.isPropertyAccessExpression(tag) && ts.isIdentifier(tag.expression) && namespaces.has(tag.expression.text) && !shadowed(ts, tag.expression))
|
|
78
|
+
return { name: tag.name.text };
|
|
79
|
+
return null;
|
|
80
|
+
};
|
|
81
|
+
const flag = (node, code, reason, component) => {
|
|
82
|
+
findings.push({ file, ...sourceLocation(source, node), code, reason, component, source: node.getText(source).slice(0, 200) });
|
|
83
|
+
};
|
|
84
|
+
const rewrites = [];
|
|
85
|
+
const visitElement = (element) => {
|
|
86
|
+
const opening = ts.isJsxElement(element) ? element.openingElement : element;
|
|
87
|
+
const resolved = resolve(opening.tagName);
|
|
88
|
+
if (resolved === null)
|
|
89
|
+
return;
|
|
90
|
+
const { name } = resolved;
|
|
91
|
+
const attributes = opening.attributes.properties;
|
|
92
|
+
const hasSpread = attributes.some(ts.isJsxSpreadAttribute);
|
|
93
|
+
if (NO_CLASS_NAME.has(name) && attributes.some(attribute => ts.isJsxAttribute(attribute) && attribute.name.getText(source) === 'className')) {
|
|
94
|
+
flag(opening, 'CLASS_NAME_REMOVED', `${name} takes no className: the structure and the role props decide the look; remove it, or state the role with the component that owns it`, name);
|
|
95
|
+
}
|
|
96
|
+
if (name === 'Tooltip' && attributes.some(attribute => ts.isJsxAttribute(attribute) && ['isDisabledTrigger', 'triggerDescribedBy'].includes(attribute.name.getText(source)))) {
|
|
97
|
+
flag(opening, 'TOOLTIP_DISABLED_TRIGGER', 'Tooltip no longer wraps a disabled control: give the Button `disabledReason`, which shows these words itself', name);
|
|
98
|
+
}
|
|
99
|
+
if (!ts.isJsxElement(element))
|
|
100
|
+
return;
|
|
101
|
+
const children = element.children.filter(child => !(ts.isJsxText(child) && child.containsOnlyTriviaWhiteSpaces));
|
|
102
|
+
if (!children.length)
|
|
103
|
+
return;
|
|
104
|
+
const toProp = CHILD_TO_PROP.get(name);
|
|
105
|
+
if (toProp !== undefined) {
|
|
106
|
+
if (hasSpread) {
|
|
107
|
+
flag(element, 'SPREAD_PROPS', `${name} takes a spread: move the child into \`${toProp.prop}\` by hand`, name);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
let value = null;
|
|
111
|
+
const only = children.length === 1 ? children[0] : undefined;
|
|
112
|
+
if (toProp.text === true) {
|
|
113
|
+
if (only !== undefined && ts.isJsxText(only))
|
|
114
|
+
value = `{${JSON.stringify(jsxText(only.getText(source)))}}`;
|
|
115
|
+
else if (only !== undefined && ts.isJsxExpression(only) && only.expression !== undefined)
|
|
116
|
+
value = `{${only.expression.getText(source)}}`;
|
|
117
|
+
else {
|
|
118
|
+
flag(element, 'CHILD_NOT_TEXT', `${name} \`${toProp.prop}\` is a string: the child is not plain text, move it by hand`, name);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else if (only !== undefined && ts.isJsxExpression(only) && only.expression !== undefined)
|
|
123
|
+
value = `{${only.expression.getText(source)}}`;
|
|
124
|
+
else if (only !== undefined && ts.isJsxText(only))
|
|
125
|
+
value = `{${JSON.stringify(jsxText(only.getText(source)))}}`;
|
|
126
|
+
else
|
|
127
|
+
value = `{<>${element.children.map(child => source.text.slice(child.pos, child.end)).join('')}</>}`;
|
|
128
|
+
if (only !== undefined && (ts.isJsxElement(only) || ts.isJsxSelfClosingElement(only) || ts.isJsxFragment(only)))
|
|
129
|
+
value = `{${only.getText(source)}}`;
|
|
130
|
+
const head = opening.getText(source).replace(/>$/, '').trimEnd();
|
|
131
|
+
const replacement = `${head} ${toProp.prop}=${value} />`;
|
|
132
|
+
rewrites.push({ element, edit: { start: element.getStart(source), end: element.end, text: replacement }, change: { file, ...sourceLocation(source, element), code: 'CHILD_TO_PROP', from: `<${name}>…</${name}>`, to: `<${name} ${toProp.prop}={…} />` } });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (LABEL_ONLY.has(name)) {
|
|
136
|
+
const first = children[0];
|
|
137
|
+
const textual = children.length === 1 && first !== undefined && (ts.isJsxText(first) || ts.isJsxExpression(first));
|
|
138
|
+
if (!textual)
|
|
139
|
+
flag(element, 'LABEL_NOT_TEXT', `${name} takes one text label as its child; its content is not plain text, move the glyph to startContent or endContent and keep the words`, name);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (NO_CHILDREN.has(name))
|
|
143
|
+
flag(element, 'TAKES_NO_CHILDREN', `${name} draws a fixed anatomy from its props and takes no children: place the content in the prop or slot that names its role`, name);
|
|
144
|
+
};
|
|
145
|
+
const innermost = new Set();
|
|
146
|
+
const visit = (node) => {
|
|
147
|
+
if (ts.isJsxElement(node))
|
|
148
|
+
visitElement(node);
|
|
149
|
+
else if (ts.isJsxSelfClosingElement(node))
|
|
150
|
+
visitElement(node);
|
|
151
|
+
ts.forEachChild(node, visit);
|
|
152
|
+
};
|
|
153
|
+
visit(source);
|
|
154
|
+
// Only a rewrite with no other rewrite inside it runs in this pass; the next pass takes the parent.
|
|
155
|
+
for (const entry of rewrites)
|
|
156
|
+
if (!rewrites.some(other => other !== entry && other.edit.start >= entry.edit.start && other.edit.end <= entry.edit.end))
|
|
157
|
+
innermost.add(entry.element);
|
|
158
|
+
const applied = rewrites.filter(entry => innermost.has(entry.element)).sort((a, b) => b.edit.start - a.edit.start);
|
|
159
|
+
let output = text;
|
|
160
|
+
for (const entry of applied) {
|
|
161
|
+
output = output.slice(0, entry.edit.start) + entry.edit.text + output.slice(entry.edit.end);
|
|
162
|
+
changes.push(entry.change);
|
|
163
|
+
}
|
|
164
|
+
return { text: output, changes, findings };
|
|
165
|
+
}
|
|
166
|
+
function sourceFiles(directory) {
|
|
167
|
+
return fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => {
|
|
168
|
+
const target = path.join(directory, entry.name);
|
|
169
|
+
if (entry.isDirectory())
|
|
170
|
+
return GENERATED.has(entry.name) ? [] : sourceFiles(target);
|
|
171
|
+
return entry.isFile() && /\.[jt]sx$/i.test(entry.name) ? [target] : [];
|
|
172
|
+
}).sort();
|
|
173
|
+
}
|
|
174
|
+
/** Preview or apply the safe rewrites in one app's FE tree (`fe/`, or `src/` and `stories/` of a single-side app); every unresolved case is kept in the returned report. */
|
|
175
|
+
export function migrateTierBinding({ root, write = false, ts = loadTypescript(root, PACKAGE_ROOT) }) {
|
|
176
|
+
if (!ts)
|
|
177
|
+
throw new Error('tier-binding needs TypeScript installed in the app or @starci/hfs development checkout');
|
|
178
|
+
const folders = ['fe', 'src', 'stories'].map(name => path.join(root, name)).filter(folder => fs.statSync(folder, { throwIfNoEntry: false })?.isDirectory());
|
|
179
|
+
if (!folders.length)
|
|
180
|
+
throw new Error('tier-binding runs at an app root with a fe/ directory');
|
|
181
|
+
const report = { schema: 'starci/tier-binding-report@1', root, write, filesScanned: 0, filesChanged: [], changes: [], findings: [] };
|
|
182
|
+
for (const target of folders.flatMap(sourceFiles)) {
|
|
183
|
+
const file = path.relative(root, target).split(path.sep).join('/');
|
|
184
|
+
const original = fs.readFileSync(target, 'utf8');
|
|
185
|
+
const result = transformTierBinding({ ts, text: original, file });
|
|
186
|
+
report.filesScanned += 1;
|
|
187
|
+
report.changes.push(...result.changes);
|
|
188
|
+
report.findings.push(...result.findings);
|
|
189
|
+
if (result.text !== original) {
|
|
190
|
+
report.filesChanged.push(file);
|
|
191
|
+
if (write)
|
|
192
|
+
fs.writeFileSync(target, result.text);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return report;
|
|
196
|
+
}
|
|
197
|
+
const cell = (value) => String(value).replaceAll('|', '\\|').replaceAll('\n', ' ');
|
|
198
|
+
/** Render the migration and what is left for a person as Markdown; JSON consumers read the same report object. */
|
|
199
|
+
export function tierBindingMarkdown(report) {
|
|
200
|
+
const out = ['# tier-binding', '', `Scanned ${report.filesScanned} files, ${report.filesChanged.length} ${report.write ? 'changed' : 'would change'}, ${report.changes.length} rewrites, ${report.findings.length} left for a person.`, ''];
|
|
201
|
+
if (report.findings.length) {
|
|
202
|
+
out.push('## Left for a person', '', '| location | code | reason |', '|---|---|---|');
|
|
203
|
+
for (const finding of report.findings)
|
|
204
|
+
out.push(`| ${cell(`${finding.file}:${finding.line}:${finding.column}`)} | ${finding.code} | ${cell(finding.reason)} |`);
|
|
205
|
+
out.push('');
|
|
206
|
+
}
|
|
207
|
+
if (report.changes.length) {
|
|
208
|
+
out.push('## Rewrites', '', '| location | code | from | to |', '|---|---|---|---|');
|
|
209
|
+
for (const item of report.changes)
|
|
210
|
+
out.push(`| ${cell(`${item.file}:${item.line}:${item.column}`)} | ${item.code} | \`${cell(item.from)}\` | \`${cell(item.to)}\` |`);
|
|
211
|
+
out.push('');
|
|
212
|
+
}
|
|
213
|
+
return `${out.join('\n')}\n`;
|
|
214
|
+
}
|
package/package.json
CHANGED
|
@@ -171,10 +171,6 @@ shapes:
|
|
|
171
171
|
Both opener paths default visibility to wide; an explicit visibility also resolves to wide.
|
|
172
172
|
Under viewport="fixed",
|
|
173
173
|
navigationTrack defaults to intrinsic so a collapsed sidebar leaves no empty track.
|
|
174
|
-
- renderer: NavigationFeatureNav
|
|
175
|
-
role: |
|
|
176
|
-
The navbar may use this renderer or the WorkspaceShell header with TopBar. Its family CSS
|
|
177
|
-
supplies sticky top placement, a 4rem band, a solid surface and a full-width bottom separator.
|
|
178
174
|
- renderer: Sidebar
|
|
179
175
|
role: |
|
|
180
176
|
Desktop navigation uses the expanded or collapsed rail; the left mobile Drawer composes
|
|
@@ -201,10 +197,10 @@ shapes:
|
|
|
201
197
|
- id: workspace-identity
|
|
202
198
|
implements: [FE-APP-SHELL-2]
|
|
203
199
|
compose: |
|
|
204
|
-
IdentityLockup
|
|
200
|
+
IdentityLockup is itself the pressable (size="control"). Its square logo mark has a
|
|
205
201
|
transparent background, no radius and object-fit contain; without an image it uses Avatar's
|
|
206
202
|
bundled DiceBear initials. The title
|
|
207
|
-
has a muted role subtitle and no caret.
|
|
203
|
+
has a muted role subtitle and no caret. The lockup has no button shape or hover effect;
|
|
208
204
|
press uses scale(0.96), focus remains visible and reduced motion removes the zoom.
|
|
209
205
|
- id: navbar-right-cluster
|
|
210
206
|
implements: [FE-APP-SHELL-3]
|
|
@@ -343,7 +339,7 @@ guidance:
|
|
|
343
339
|
LAYOUT-1 case-10 (owner:joined-multi-pane): the selected family realizes one touching pane
|
|
344
340
|
surface with vertical Dividers. SURFACE-1 case-4 (owner:scroll-region-background-tone):
|
|
345
341
|
VerticalScrollRegion tone="background" under messages and log entries, with the header and
|
|
346
|
-
composer on the surface. ACTION-3 case-5 (owner:hover-actions-floating-toolbar):
|
|
342
|
+
composer on the surface. ACTION-3 case-5 (owner:hover-actions-floating-toolbar): MessageBubble's
|
|
347
343
|
public floating toolbar owns the IconButtons and tooltips beside the text; apps do not target
|
|
348
344
|
starci-core internals. CTA-6 case-4 (owner:button-variant-follows-intent): ordinary repeated
|
|
349
345
|
item commands are neutral and render ghost.
|
|
@@ -351,23 +347,22 @@ guidance:
|
|
|
351
347
|
ownership: FE-RULE-OWNERSHIP
|
|
352
348
|
implements: [ANATOMY-2, PADDING-0]
|
|
353
349
|
requirement: |
|
|
354
|
-
SurfaceCard (always painted
|
|
355
|
-
AlertDialog, Drawer and Popover establish the surface nesting context; Rail and
|
|
356
|
-
|
|
350
|
+
SurfaceCard (always painted, of any kind, the accordion kind when painted), Sidebar, Dialog,
|
|
351
|
+
AlertDialog, Drawer and Popover establish the surface nesting context; Rail and
|
|
352
|
+
an unpainted accordion SurfaceCard pass their parent's context through. Input, SearchField, Textarea,
|
|
357
353
|
NumberField, DateField, TimeField, Select, ComboBox, DatePicker, DateRangePicker, Checkbox,
|
|
358
354
|
CheckboxGroup, RadioGroup, OtpInput and PressableField resolve variant="secondary" inside it and
|
|
359
355
|
variant="primary" outside. Compose the surface and omit the field variant.
|
|
360
356
|
Switch and Slider have no layer variant; a Radio follows its RadioGroup. PaneGrid's
|
|
361
|
-
collapsed-pane drawer also establishes the context. SurfaceCard and bounded
|
|
357
|
+
collapsed-pane drawer also establishes the context. SurfaceCard and a bounded accordion SurfaceCard derive
|
|
362
358
|
data-grammar-surface-depth from the parent context. The top layer is raised and a nested layer is
|
|
363
359
|
subordinate; never raise both. The family DNA owns their surface paint.
|
|
364
|
-
|
|
365
|
-
anywhere in app code and primary Tabs inside a painted surface; FE_GRAMMAR_LAYOUT_ONLY rejects
|
|
360
|
+
The field `variant` is not in the prop types, so an app cannot set it; FE_GRAMMAR_LAYOUT_ONLY rejects
|
|
366
361
|
application paint utilities, arbitrary paint properties and inline style paint.
|
|
367
362
|
DRAW_NESTED_VARIANT checks the corresponding rendered variant and
|
|
368
363
|
surface stamps.
|
|
369
364
|
ListBox resolves inset="panel" inside that context; callers may select the same value explicitly
|
|
370
|
-
elsewhere. Its rows and sections captions share the panel edge.
|
|
365
|
+
elsewhere. Its rows and sections captions share the panel edge. The list kind of SurfaceCard owns its list
|
|
371
366
|
shell with no added inset; SurfaceCard composition="joined" owns flush bands. A grouping wrapper
|
|
372
367
|
adds no padding. An extra px-*, ps-* or pe-* around ListBox or a section caption stacks the inset
|
|
373
368
|
and fails FE_LIST_PANEL_PADDING.
|
|
@@ -440,27 +435,27 @@ guidance:
|
|
|
440
435
|
uiRules: [STATE-2]
|
|
441
436
|
render: |
|
|
442
437
|
The ready tree, drawn from placeholder data of the slot's type, in the shape the unit's
|
|
443
|
-
`xDefaultState` names, with
|
|
438
|
+
`xDefaultState` names, with isLoading on every owner: same card, same bands, same row count
|
|
444
439
|
(idiom pending-same-tree). Never a spinner and never a different tree. The skeleton renders
|
|
445
440
|
inert geometry and states no value; an unknown number is a bar, never a rendered zero.
|
|
446
441
|
skeletonMap:
|
|
447
442
|
- part: Text line or Heading
|
|
448
|
-
render: "`
|
|
443
|
+
render: "`isLoading` on the owner: one bar at that type-scale step's line height, as long as the placeholder text"
|
|
449
444
|
- part: Media (Image, MediaFrame viewport)
|
|
450
445
|
render: "`Skeleton shape=\"rect\"` at the frame's `ratio` (square, landscape, wide); `shape=\"circle\"` for an avatar. A block at the ratio, never a collapsed box"
|
|
451
446
|
- part: Button
|
|
452
447
|
render: |
|
|
453
448
|
A bar at the Button's height with the Button's pill corner (family.yaml button-pill):
|
|
454
|
-
`
|
|
449
|
+
`isLoading` where the installed Button publishes it, otherwise `Skeleton shape="rect"` in
|
|
455
450
|
the Button's box. It never fires (idiom pending-same-tree action-must-not-fire)
|
|
456
451
|
- part: A region that exists only on ready data (a disclosure of the loaded lines)
|
|
457
|
-
render: Absent while
|
|
452
|
+
render: Absent while isLoading (STATE-3)
|
|
458
453
|
- status: unauthorized
|
|
459
454
|
uiRules: [STATE-3]
|
|
460
455
|
render: |
|
|
461
456
|
401 is its own status, never forbidden and never an error. The slot shows one sign-in prompt in
|
|
462
457
|
place: `EmptyNotice message={labels.unauthorized} actionLabel={labels.signIn}
|
|
463
|
-
|
|
458
|
+
onAction={signIn}`, where `signIn` opens the product's sign-in route and
|
|
464
459
|
returns to this address. The prompt discloses none of the slot's data. A secondary slot hides, as
|
|
465
460
|
forbidden does, so a page whose reads all need a session shows the prompt once, in its primary slot.
|
|
466
461
|
- status: forbidden
|
|
@@ -475,7 +470,7 @@ guidance:
|
|
|
475
470
|
render: |
|
|
476
471
|
Retry at the slot, re-running that slot's read only (the query's `mutate`), never a page
|
|
477
472
|
reload. Block form, where the slot is a region: `EmptyNotice message={labels.error}
|
|
478
|
-
actionLabel={labels.retry}
|
|
473
|
+
actionLabel={labels.retry} onAction={retry}` with `isActionPending`
|
|
479
474
|
bound while the retry runs. Inline form, where the slot is one line inside a row or band too
|
|
480
475
|
small for a notice: `Alert tone="negative"` in the slot's place, carrying the retry action. A
|
|
481
476
|
command that is unsafe to repeat is not retried (FEEDBACK-2 case-4); a rejected field value
|
|
@@ -486,7 +481,7 @@ guidance:
|
|
|
486
481
|
`EmptyNotice message={labels.empty}` with the slot's own empty sentence; `actionLabel` and
|
|
487
482
|
`onAction` only when a real next step exists, otherwise both are omitted.
|
|
488
483
|
- status: ready
|
|
489
|
-
render: The slot's items through the same tree,
|
|
484
|
+
render: The slot's items through the same tree, isLoading false.
|
|
490
485
|
notFound: |
|
|
491
486
|
A route whose resource does not exist calls `notFound()` in its route adapter (FE-FUNCTION-7
|
|
492
487
|
case-2). 404 is never a page state, a shape or a slot status.
|
|
@@ -615,9 +610,6 @@ presentation:
|
|
|
615
610
|
- component: HorizontalScrollRegion
|
|
616
611
|
element: root
|
|
617
612
|
rule: PADDING-1
|
|
618
|
-
- component: NavigationFeatureNav
|
|
619
|
-
element: primary
|
|
620
|
-
rule: PADDING-3
|
|
621
613
|
- component: OtpInput
|
|
622
614
|
element: root
|
|
623
615
|
rule: PADDING-1
|
|
@@ -669,22 +661,22 @@ presentation:
|
|
|
669
661
|
- component: Subnav
|
|
670
662
|
element: root
|
|
671
663
|
rule: PADDING-3
|
|
672
|
-
- component:
|
|
664
|
+
- component: SurfaceCard (accordion kind)
|
|
673
665
|
element: accordion panel
|
|
674
666
|
rule: PADDING-0
|
|
675
|
-
- component:
|
|
667
|
+
- component: SurfaceCard (accordion kind)
|
|
676
668
|
element: accordion trigger
|
|
677
669
|
rule: PADDING-4
|
|
678
|
-
- component:
|
|
670
|
+
- component: SurfaceCard (accordion kind)
|
|
679
671
|
element: body padding-top
|
|
680
672
|
rule: PADDING-0
|
|
681
|
-
- component:
|
|
673
|
+
- component: SurfaceCard (accordion kind)
|
|
682
674
|
element: body padding-bottom
|
|
683
675
|
rule: PADDING-3
|
|
684
|
-
- component:
|
|
676
|
+
- component: SurfaceCard (accordion kind)
|
|
685
677
|
element: body padding-right
|
|
686
678
|
rule: PADDING-4
|
|
687
|
-
- component:
|
|
679
|
+
- component: SurfaceCard (accordion kind)
|
|
688
680
|
element: body padding-left
|
|
689
681
|
rule: PADDING-5
|
|
690
682
|
- component: SurfaceCard
|
|
@@ -693,7 +685,7 @@ presentation:
|
|
|
693
685
|
- component: SurfaceCard
|
|
694
686
|
element: content, composition="joined"
|
|
695
687
|
rule: PADDING-0
|
|
696
|
-
- component:
|
|
688
|
+
- component: SurfaceCard (list kind)
|
|
697
689
|
element: root
|
|
698
690
|
rule: PADDING-0
|
|
699
691
|
- component: Tabs
|
|
@@ -776,7 +768,7 @@ presentation:
|
|
|
776
768
|
render: '`composition="joined"` already sets `0`'
|
|
777
769
|
- id: case-3
|
|
778
770
|
when: A Grammar list sits inside a panel or its rows own their own inset
|
|
779
|
-
owner: "`
|
|
771
|
+
owner: "`SurfaceCard` list kind / `ListBox`"
|
|
780
772
|
render: ListBox resolves inset="panel" in a surface nesting context, or callers select it explicitly. Its
|
|
781
773
|
row boxes align with the panel content edge, like the search field and tabs boxes, without
|
|
782
774
|
negative-margin bleed (owner:panel-list-box-edge). The list retains HeroUI p-1; items retain
|
|
@@ -842,7 +834,7 @@ presentation:
|
|
|
842
834
|
render: Compose the card; no padding
|
|
843
835
|
- id: case-3
|
|
844
836
|
when: A disclosure trigger and its expanded panel
|
|
845
|
-
owner: "`
|
|
837
|
+
owner: "`SurfaceCard` accordion kind"
|
|
846
838
|
render: Compose the card; no padding
|
|
847
839
|
- id: case-4
|
|
848
840
|
when: An empty-state notice
|
|
@@ -995,9 +987,6 @@ presentation:
|
|
|
995
987
|
- component: Text
|
|
996
988
|
element: root, size="md"
|
|
997
989
|
rule: FONT-3
|
|
998
|
-
- component: Text
|
|
999
|
-
element: root, size="metric-lead"
|
|
1000
|
-
rule: FONT-5
|
|
1001
990
|
- component: Text
|
|
1002
991
|
element: root, size="sm"
|
|
1003
992
|
rule: FONT-2
|
|
@@ -1016,10 +1005,10 @@ presentation:
|
|
|
1016
1005
|
- component: Tooltip
|
|
1017
1006
|
element: content
|
|
1018
1007
|
rule: FONT-1
|
|
1019
|
-
- component:
|
|
1008
|
+
- component: SurfaceCard (list kind)
|
|
1020
1009
|
element: fact, label is visible and fact is present, depth="nested"
|
|
1021
1010
|
rule: FONT-1
|
|
1022
|
-
- component:
|
|
1011
|
+
- component: SurfaceCard (list kind)
|
|
1023
1012
|
element: fact, label is visible and fact is present, depth!="nested"
|
|
1024
1013
|
rule: FONT-2
|
|
1025
1014
|
guidance:
|
|
@@ -1029,11 +1018,14 @@ presentation:
|
|
|
1029
1018
|
Weight is a second axis, not a position on the size scale. Its public set is closed.
|
|
1030
1019
|
| Prop | Rendered | Use for |
|
|
1031
1020
|
| --- | --- | --- |
|
|
1032
|
-
|
|
|
1033
|
-
|
|
|
1034
|
-
| `
|
|
1021
|
+
| Where | Rendered | Use for |
|
|
1022
|
+
| --- | --- | --- |
|
|
1023
|
+
| `Text`, no prop | `font-normal`, the default | Reading copy and ordinary facts |
|
|
1024
|
+
| `Heading` levels 3 and 4, and labels | `font-medium` | A compact label, or a title above its own description |
|
|
1025
|
+
| `Text isStrong` | `font-semibold` | A value or a term that must be found first inside its line |
|
|
1035
1026
|
Weight refines a rank the application has already chosen. It never manufactures a new hierarchy
|
|
1036
|
-
level, and it never substitutes for a heading
|
|
1027
|
+
level, and it never substitutes for a heading: a group is headed by its `Section` title or its
|
|
1028
|
+
card label, never by a strong `Text`. `Heading` applies its own weight per level and takes
|
|
1037
1029
|
no weight prop.
|
|
1038
1030
|
Bold beyond `semibold` has no public prop. Treat an intent that needs it as a gap rather than writing
|
|
1039
1031
|
a class.
|
|
@@ -1089,8 +1081,8 @@ presentation:
|
|
|
1089
1081
|
cases:
|
|
1090
1082
|
- id: case-1
|
|
1091
1083
|
when: The single leading metric or short value that is the main fact of its region
|
|
1092
|
-
owner: "`
|
|
1093
|
-
render: '`<
|
|
1084
|
+
owner: "`DescriptionList` layout=\"stat-strip\""
|
|
1085
|
+
render: '`<DescriptionList layout="stat-strip" items={[{ id: "rate", term: "Resolved", description: "84%" }]} />`'
|
|
1094
1086
|
notThisRule: sentences, decoration, or imitating a heading. It creates no heading semantics.
|
|
1095
1087
|
- rule: FONT-6
|
|
1096
1088
|
title: "`text-4xl` / `leading-tight`"
|
|
@@ -1105,7 +1097,7 @@ anatomyObservations:
|
|
|
1105
1097
|
lane: ui
|
|
1106
1098
|
kind: proven-template
|
|
1107
1099
|
summary: |-
|
|
1108
|
-
Verified in the draw lab against the real
|
|
1100
|
+
Verified in the draw lab against the real list SurfaceCard render — the prompt shape that
|
|
1109
1101
|
produced a direction both faithful and beautiful:
|
|
1110
1102
|
1. Frame: flat product UI screenshot, size, light theme.
|
|
1111
1103
|
2. SUBJECT: screen name + product name.
|
|
@@ -1122,11 +1114,9 @@ anatomyObservations:
|
|
|
1122
1114
|
lane: ui
|
|
1123
1115
|
kind: anatomy-detail
|
|
1124
1116
|
summary: |-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
renders its label and fact as siblings on the page background ABOVE the list shell —
|
|
1129
|
-
the card bounds only the rows, the footer sits below it.
|
|
1117
|
+
Every `SurfaceCard` kind except `tile` places its label outside: an unpainted row above the material
|
|
1118
|
+
with the fact and the end action, and the card bounds only the content, the footer note below it.
|
|
1119
|
+
A prompt that draws the label inside the card invents a placement the grammar does not have.
|
|
1130
1120
|
- id: list-separator-bleed
|
|
1131
1121
|
lane: ui
|
|
1132
1122
|
kind: anatomy-detail
|
|
@@ -1140,7 +1130,7 @@ anatomyObservations:
|
|
|
1140
1130
|
lane: ui
|
|
1141
1131
|
kind: anatomy-detail
|
|
1142
1132
|
summary: |-
|
|
1143
|
-
An interactive row in a
|
|
1133
|
+
An interactive row in a list SurfaceCard is the grammar `ListRow` (@starci/grammar 0.9.1), never an
|
|
1144
1134
|
app-owned `<li>` and never a second `<ul>` nested in the card (the card already renders the list,
|
|
1145
1135
|
so a nested list loses the row inset and the separator - a defect that leaves rows flush against
|
|
1146
1136
|
the card edge). Its slots, in reading order: `leading`
|
|
@@ -69,8 +69,8 @@ shapes:
|
|
|
69
69
|
Keep: which sections exist and which one is the next step. Never crosses over: layout grid, card
|
|
70
70
|
chrome, or a section that would need a renderer the family does not publish.
|
|
71
71
|
composition:
|
|
72
|
-
- renderer:
|
|
73
|
-
role: The greeting or section title
|
|
72
|
+
- renderer: Section
|
|
73
|
+
role: The greeting or section title
|
|
74
74
|
- renderer: SurfaceCard
|
|
75
75
|
role: |
|
|
76
76
|
One top-level sticker for the next step (`isHighlight` on that card only), holding the title
|
|
@@ -78,7 +78,7 @@ shapes:
|
|
|
78
78
|
Button variant="primary" — the view's single pink fill
|
|
79
79
|
- renderer: "Progress / ProgressCircle / Meter"
|
|
80
80
|
role: Only a verified value with a truthful `label` (ACCENT-4); an unknown value is not drawn
|
|
81
|
-
- renderer:
|
|
81
|
+
- renderer: SurfaceCard (list kind)
|
|
82
82
|
role: Peers (recent items, suggestions) as static rows; no second primary Button
|
|
83
83
|
- renderer: "Skeleton / EmptyNotice"
|
|
84
84
|
role: Pending is the same tree at rest (Skeleton in place of content); empty or failed is EmptyNotice
|
|
@@ -182,10 +182,10 @@ shapes:
|
|
|
182
182
|
role: The query, labelled
|
|
183
183
|
- renderer: TagGroup
|
|
184
184
|
role: Filters as outlined chips; selected chips turn pink and lift
|
|
185
|
-
- renderer: "ListBox /
|
|
185
|
+
- renderer: "ListBox / SurfaceCard (list kind)"
|
|
186
186
|
role: |
|
|
187
187
|
ListBox when picking is the task (yellow hover wash, pink focus ring, soft-pink selected row
|
|
188
|
-
with a pink leading edge);
|
|
188
|
+
with a pink leading edge); the list kind of SurfaceCard when rows only report facts
|
|
189
189
|
- renderer: Pagination
|
|
190
190
|
role: Current page is the raised pink pill
|
|
191
191
|
- renderer: "Skeleton / EmptyNotice"
|
|
@@ -31,42 +31,42 @@ pins:
|
|
|
31
31
|
source: packages/event-stream/package.json
|
|
32
32
|
why: 'the one server-sent-events client of every front end (fe-stack event-stream, owner:event-stream-package): the transport client layer (modules/api/sse/clients) creates one instance for the app with the bearer token source and the stream operations follow on it; it carries Last-Event-ID resume, the capped doubling backoff, the heartbeat timeout, the single 401 renewal and the useSWRSubscription adapter, so no app keeps a hand-written reader.'
|
|
33
33
|
'@starci/grammar':
|
|
34
|
-
version: 0.
|
|
34
|
+
version: 0.21.0
|
|
35
35
|
group: starci
|
|
36
36
|
install: registry
|
|
37
37
|
side: fe
|
|
38
38
|
source: packages/grammar/package.json
|
|
39
39
|
why: 'the grammar front ends read: the brand layer sets `--font-sans` and `--font-mono` and the grammar reads them; it carries the Input tel kind, the copyable and read-only Input, the IconButton disclosure props and count badge, ChatBubble, FillViewport, Textarea mentions and Enter-to-send, the leading PrimaryRailLayout rail, the plate-less Image, ListRow, the soft Alert, Progress steps, the floating primary IconButton, FlowGraph, the STATE-8 loading views and the bordered selectable TagGroup chips. The runtime source version wins for a @starci package.'
|
|
40
40
|
'@starci/eslint-canon-be':
|
|
41
|
-
version: 3.11.
|
|
41
|
+
version: 3.11.7
|
|
42
42
|
group: starci
|
|
43
43
|
install: registry
|
|
44
44
|
side: be
|
|
45
45
|
source: packages/eslint-canon-be/package.json
|
|
46
46
|
why: 'the back-end ESLint canon: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; `starciBeConfig({ hfs: loadHfs(import.meta.url) })` is the typed factory and the BE-CONVENTION laws are its rules.'
|
|
47
47
|
'@starci/eslint-canon-fe':
|
|
48
|
-
version: 9.
|
|
48
|
+
version: 9.9.0
|
|
49
49
|
group: starci
|
|
50
50
|
install: registry
|
|
51
51
|
side: fe
|
|
52
52
|
source: packages/eslint-canon-fe/package.json
|
|
53
53
|
why: 'the front-end ESLint canon: `loadHfs(import.meta.url)` of fe/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the fe side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; the front end has no tests (FE_NO_TESTS R97 in hfs) and no-vietnamese-in-source (R91) applies.'
|
|
54
54
|
'@starci/eslint-grammar-core':
|
|
55
|
-
version: 0.
|
|
55
|
+
version: 0.21.0
|
|
56
56
|
group: starci
|
|
57
57
|
install: registry
|
|
58
58
|
side: fe
|
|
59
59
|
source: packages/eslint-grammar-core/package.json
|
|
60
60
|
why: 'the StarCi Grammar component API lint, versioned with @starci/grammar; front ends compose this family package with the universal FE canon.'
|
|
61
61
|
'@starci/eslint-grammar-offset-pop':
|
|
62
|
-
version: 0.
|
|
62
|
+
version: 0.21.0
|
|
63
63
|
group: starci
|
|
64
64
|
install: registry
|
|
65
65
|
side: fe
|
|
66
66
|
source: packages/eslint-grammar-offset-pop/package.json
|
|
67
67
|
why: 'the Offset Pop Grammar component API lint, versioned with @starci/grammar; its shared component contract comes from the StarCi family lint without copying rules.'
|
|
68
68
|
'@starci/stylelint-canon':
|
|
69
|
-
version: 2.
|
|
69
|
+
version: 2.5.0
|
|
70
70
|
group: starci
|
|
71
71
|
install: registry
|
|
72
72
|
side: fe
|
|
@@ -106,7 +106,7 @@ pins:
|
|
|
106
106
|
source: packages/ai-router/package.json
|
|
107
107
|
why: 'the AI pool router of a back end: a NestJS module that routes OpenAI-compatible chat, stream and embeddings calls across keys, models and providers, with @starci/ai-router/testing for its specs; @starci/jest-preset compiles it as an ESM-only dependency.'
|
|
108
108
|
'@starci/hfs':
|
|
109
|
-
version: 4.
|
|
109
|
+
version: 4.17.0
|
|
110
110
|
group: starci
|
|
111
111
|
install: registry
|
|
112
112
|
side: both
|