@starci/hfs 4.0.7 → 4.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +10 -9
  3. package/bin/hfs.mjs +4 -4
  4. package/lint/run.mjs +1 -1
  5. package/package.json +2 -2
  6. package/runtime/engine/runtime-root.mjs +5 -0
  7. package/runtime/engine/yaml.mjs +3 -3
  8. package/runtime/knowledge/hfs/canon-pins.yaml +10 -10
  9. package/runtime/knowledge/hfs/slots.yaml +5 -5
  10. package/runtime/knowledge/patterns/fe/folder.yaml +2 -2
  11. package/runtime/knowledge/sonar-gate.yaml +2 -2
  12. package/runtime/modules/kernel/failure-codes.yaml +12 -2
  13. package/runtime/scripts/api/fs/lib.mjs +6 -0
  14. package/runtime/scripts/api/fs/rmdir-link.mjs +1 -1
  15. package/runtime/scripts/{lib → api/fs}/safe-remove.mjs +31 -82
  16. package/runtime/scripts/api/git/lib.mjs +1 -1
  17. package/runtime/scripts/api/sops/decrypt.mjs +1 -1
  18. package/runtime/scripts/{lib/hfs-allows.mjs → hfs/allows.mjs} +3 -3
  19. package/runtime/scripts/{checks → hfs}/architecture/backend.mjs +1 -1
  20. package/runtime/scripts/{checks → hfs}/architecture/config.mjs +2 -2
  21. package/runtime/scripts/{checks → hfs}/architecture/connection-map.mjs +1 -1
  22. package/runtime/scripts/{checks → hfs}/architecture/contract-fixture-guard.mjs +1 -1
  23. package/runtime/scripts/{checks → hfs}/architecture/fe-slot-allows.mjs +3 -3
  24. package/runtime/scripts/{checks → hfs}/architecture/feature-shape.mjs +1 -1
  25. package/runtime/scripts/{checks → hfs}/architecture/hfs-graph.mjs +1 -1
  26. package/runtime/scripts/{checks → hfs}/architecture/hfs.mjs +3 -3
  27. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +96 -0
  28. package/runtime/scripts/{checks → hfs}/architecture/next-data.mjs +18 -105
  29. package/runtime/scripts/{checks → hfs}/architecture/required-files.mjs +1 -1
  30. package/runtime/scripts/{checks → hfs}/architecture/surface.mjs +1 -1
  31. package/runtime/scripts/{checks → hfs}/architecture/test-world-files.mjs +2 -2
  32. package/runtime/scripts/{checks → hfs}/architecture/typescript.mjs +1 -1
  33. package/runtime/scripts/{checks → hfs}/architecture.mjs +2 -2
  34. package/runtime/scripts/{lib/hfs-check.mjs → hfs/check.mjs} +33 -25
  35. package/runtime/scripts/hfs/manifest-shape.mjs +170 -0
  36. package/runtime/scripts/{lib/hfs-path-findings.mjs → hfs/path-findings.mjs} +6 -6
  37. package/runtime/scripts/{lib/hfs-rules → hfs/rules}/contract.mjs +3 -2
  38. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +66 -0
  39. package/runtime/scripts/{lib/hfs-rules → hfs/rules}/integration-specs.mjs +1 -1
  40. package/runtime/scripts/{lib/hfs-rules → hfs/rules}/secrets.mjs +2 -2
  41. package/runtime/scripts/{lib/hfs-rules → hfs/rules}/stacks.mjs +2 -2
  42. package/runtime/scripts/{lib/hfs-slots.mjs → hfs/slots.mjs} +52 -58
  43. package/runtime/scripts/{lib/hfs-tree.mjs → hfs/tree.mjs} +1 -1
  44. package/runtime/scripts/{lib/hfs-view.mjs → hfs/view.mjs} +3 -3
  45. package/runtime/scripts/lib/graphql-contract.mjs +429 -0
  46. package/runtime/scripts/lib/is-main.mjs +13 -0
  47. package/runtime/scripts/lib/language.mjs +2 -2
  48. package/runtime/scripts/lib/path-key.mjs +2 -0
  49. package/runtime/scripts/lib/stack-declaration.mjs +2 -2
  50. package/runtime/scripts/{checks/common.mjs → lib/walk.mjs} +1 -12
  51. package/scaffold/app.mjs +1 -1
  52. package/scaffold/service.mjs +2 -2
  53. package/sync/hygiene.mjs +8 -8
  54. package/sync/index.mjs +48 -11
  55. package/templates/app/quality-config/codecov.yml +1 -1
  56. package/templates/app/quality-config/sonar-project.properties +1 -1
  57. package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.ts +2 -2
  58. package/templates/fe/skeleton/apps/__app__/next.config.ts +10 -2
  59. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/error.tsx +2 -2
  60. package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/component.tsx +0 -2
  61. package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/index.tsx +1 -7
  62. package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/component.tsx +0 -2
  63. package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/index.tsx +0 -1
  64. package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/component.tsx +0 -4
  65. package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/index.tsx +1 -1
  66. package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/component.tsx +0 -4
  67. package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/index.tsx +1 -1
  68. package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/component.tsx +0 -4
  69. package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/index.tsx +1 -1
  70. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/request.ts +8 -3
  71. package/templates/fe/skeleton/apps/__app__/src/proxy.ts +1 -1
  72. package/runtime/engine/admission.mjs +0 -284
  73. package/runtime/engine/digest.mjs +0 -10
  74. package/runtime/engine/ledger-db.mjs +0 -1245
  75. package/runtime/engine/machine-db.mjs +0 -1565
  76. package/runtime/engine/migrations/machine/0001-init.sql +0 -887
  77. package/runtime/engine/migrations/machine/0002-worktrees-no-workflow-kind.sql +0 -13
  78. package/runtime/engine/migrations/machine/0003-worktrees-workflow-orca.sql +0 -21
  79. package/runtime/engine/migrations/runtime/0001-init.sql +0 -1072
  80. package/runtime/engine/migrations/runtime/0003-usage-unavailable.sql +0 -13
  81. package/runtime/engine/migrations/runtime/0004-attempt-why.sql +0 -43
  82. package/runtime/engine/migrations/runtime/0005-ended-workflow-views.sql +0 -93
  83. package/runtime/scripts/lib/artifact-hold.mjs +0 -89
  84. package/runtime/scripts/lib/artifact-store.mjs +0 -103
  85. package/runtime/scripts/lib/redact.mjs +0 -148
  86. /package/runtime/scripts/{checks → hfs}/architecture/background-unowned.mjs +0 -0
  87. /package/runtime/scripts/{checks → hfs}/architecture/client-reaches-server.mjs +0 -0
  88. /package/runtime/scripts/{checks → hfs}/architecture/clones.mjs +0 -0
  89. /package/runtime/scripts/{checks → hfs}/architecture/config-unread.mjs +0 -0
  90. /package/runtime/scripts/{checks → hfs}/architecture/constructor-deps.mjs +0 -0
  91. /package/runtime/scripts/{checks → hfs}/architecture/contracts.mjs +0 -0
  92. /package/runtime/scripts/{checks → hfs}/architecture/cross-app-duplicate.mjs +0 -0
  93. /package/runtime/scripts/{checks → hfs}/architecture/dead-exports.mjs +0 -0
  94. /package/runtime/scripts/{checks → hfs}/architecture/default-deny.mjs +0 -0
  95. /package/runtime/scripts/{checks → hfs}/architecture/doc-language.mjs +0 -0
  96. /package/runtime/scripts/{checks → hfs}/architecture/entrypoint.mjs +0 -0
  97. /package/runtime/scripts/{checks → hfs}/architecture/error-codes.mjs +0 -0
  98. /package/runtime/scripts/{checks → hfs}/architecture/error-masked.mjs +0 -0
  99. /package/runtime/scripts/{checks → hfs}/architecture/framework-pinned.mjs +0 -0
  100. /package/runtime/scripts/{checks → hfs}/architecture/frontend.mjs +0 -0
  101. /package/runtime/scripts/{checks → hfs}/architecture/hooks-are-hooks.mjs +0 -0
  102. /package/runtime/scripts/{checks → hfs}/architecture/i18n-keys.mjs +0 -0
  103. /package/runtime/scripts/{checks → hfs}/architecture/index.mjs +0 -0
  104. /package/runtime/scripts/{checks → hfs}/architecture/injection-token-exported.mjs +0 -0
  105. /package/runtime/scripts/{checks → hfs}/architecture/machine-ast.mjs +0 -0
  106. /package/runtime/scripts/{checks → hfs}/architecture/module-per-transport.mjs +0 -0
  107. /package/runtime/scripts/{checks → hfs}/architecture/owners.mjs +0 -0
  108. /package/runtime/scripts/{checks → hfs}/architecture/package-shape.mjs +0 -0
  109. /package/runtime/scripts/{checks → hfs}/architecture/reachability.mjs +0 -0
  110. /package/runtime/scripts/{checks → hfs}/architecture/register-once.mjs +0 -0
  111. /package/runtime/scripts/{checks → hfs}/architecture/registration.mjs +0 -0
  112. /package/runtime/scripts/{checks → hfs}/architecture/route-files-thin.mjs +0 -0
  113. /package/runtime/scripts/{checks → hfs}/architecture/schema-owner.mjs +0 -0
  114. /package/runtime/scripts/{checks → hfs}/architecture/source-names.mjs +0 -0
  115. /package/runtime/scripts/{checks → hfs}/architecture/sql-owner.mjs +0 -0
  116. /package/runtime/scripts/{checks → hfs}/architecture/sql-tokens.mjs +0 -0
  117. /package/runtime/scripts/{checks → hfs}/architecture/symbols.mjs +0 -0
  118. /package/runtime/scripts/{checks → hfs}/architecture/tiers.mjs +0 -0
  119. /package/runtime/scripts/{checks → hfs}/architecture/transport-owner.mjs +0 -0
  120. /package/runtime/scripts/{checks → hfs}/architecture/unit-spec-providers.mjs +0 -0
  121. /package/runtime/scripts/{lib → hfs}/repo-identity.mjs +0 -0
  122. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/deps.mjs +0 -0
  123. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/fe-no-tests.mjs +0 -0
  124. /package/runtime/scripts/{lib/hfs-rules/frontend.mjs → hfs/rules/frontend-tree.mjs} +0 -0
  125. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/lint-suppression.mjs +0 -0
  126. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/peer-integrations.mjs +0 -0
  127. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/pipeline.mjs +0 -0
  128. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/proof-commands.mjs +0 -0
  129. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/read.mjs +0 -0
  130. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/repo-local-checks.mjs +0 -0
  131. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/spec-placement.mjs +0 -0
  132. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/test-topology.mjs +0 -0
  133. /package/runtime/scripts/{checks → hfs}/typescript-programs.mjs +0 -0
@@ -0,0 +1,429 @@
1
+ // graphql-contract.mjs - the GraphQL a back-end contract snapshot holds and the checks a front-end document must pass
2
+ // against it. It reads the SDL `hfs emit-contracts` writes (`be/contracts/<service>/schema.graphql`: object, input, enum,
3
+ // scalar, interface and union types, descriptions and directives) and the executable documents a front end keeps in
4
+ // `.graphql` files (operations with variables, field arguments, object literals, inline fragments), with no dependency:
5
+ // the runtime ships no GraphQL library, and an app's own install is not always resolvable from where hfs runs.
6
+ //
7
+ // What a document is checked for: every selected field exists on its parent type, every argument it passes is declared
8
+ // and every required argument (or required input field of an object literal) is given, an object literal names only
9
+ // declared input fields, a variable passed to an argument has the argument's named type, a leaf field selects nothing
10
+ // and an object field selects something, and every declared variable is used. Validation stops at the first failure of
11
+ // each field path, so a broken document reads as a short list of named problems.
12
+
13
+ const PUNCTUATORS = new Set(['{', '}', '(', ')', '[', ']', ':', '!', '=', '$', '@', '|', '&']);
14
+ const BUILT_IN_SCALARS = new Set(['String', 'Int', 'Float', 'Boolean', 'ID']);
15
+
16
+ /** The tokens of a GraphQL source: names, punctuators, `...`, strings and numbers; comments and commas are insignificant. */
17
+ export function tokenize(source) {
18
+ const tokens = [];
19
+ let index = 0;
20
+ while (index < source.length) {
21
+ const char = source[index];
22
+ if (char === '#') {
23
+ while (index < source.length && source[index] !== '\n') index += 1;
24
+ } else if (/[\s,]/.test(char)) {
25
+ index += 1;
26
+ } else if (source.startsWith('...', index)) {
27
+ tokens.push({ kind: 'spread', value: '...' });
28
+ index += 3;
29
+ } else if (source.startsWith('"""', index)) {
30
+ const end = source.indexOf('"""', index + 3);
31
+ if (end === -1) throw new SyntaxError('unterminated block string');
32
+ tokens.push({ kind: 'string', value: source.slice(index + 3, end) });
33
+ index = end + 3;
34
+ } else if (char === '"') {
35
+ let end = index + 1;
36
+ while (end < source.length && source[end] !== '"') end += source[end] === '\\' ? 2 : 1;
37
+ tokens.push({ kind: 'string', value: source.slice(index + 1, end) });
38
+ index = end + 1;
39
+ } else if (PUNCTUATORS.has(char)) {
40
+ tokens.push({ kind: 'punct', value: char });
41
+ index += 1;
42
+ } else if (/[_A-Za-z]/.test(char)) {
43
+ const match = /^[_A-Za-z][_0-9A-Za-z]*/.exec(source.slice(index));
44
+ tokens.push({ kind: 'name', value: match[0] });
45
+ index += match[0].length;
46
+ } else if (/[-0-9]/.test(char)) {
47
+ const match = /^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/.exec(source.slice(index));
48
+ if (!match) throw new SyntaxError(`unexpected character "${char}"`);
49
+ tokens.push({ kind: 'number', value: match[0] });
50
+ index += match[0].length;
51
+ } else {
52
+ throw new SyntaxError(`unexpected character "${char}"`);
53
+ }
54
+ }
55
+ return tokens;
56
+ }
57
+
58
+ /** A cursor over tokens with the expectations both parsers share. */
59
+ function cursorOf(tokens) {
60
+ let index = 0;
61
+ const cursor = {
62
+ peek: (offset = 0) => tokens[index + offset] ?? null,
63
+ next: () => {
64
+ const token = tokens[index];
65
+ if (token === undefined) throw new SyntaxError('unexpected end of document');
66
+ index += 1;
67
+ return token;
68
+ },
69
+ is: (value) => tokens[index]?.value === value && tokens[index]?.kind !== 'string',
70
+ done: () => index >= tokens.length,
71
+ take: (value) => {
72
+ if (!cursor.is(value)) return false;
73
+ index += 1;
74
+ return true;
75
+ },
76
+ expect: (value) => {
77
+ const token = cursor.next();
78
+ if (token.value !== value || token.kind === 'string') throw new SyntaxError(`expected "${value}", found "${token.value}"`);
79
+ return token;
80
+ },
81
+ name: () => {
82
+ const token = cursor.next();
83
+ if (token.kind !== 'name') throw new SyntaxError(`expected a name, found "${token.value}"`);
84
+ return token.value;
85
+ },
86
+ };
87
+ return cursor;
88
+ }
89
+
90
+ /** A type reference: `{ name, list, nonNull, item }`. */
91
+ function parseTypeRef(cursor) {
92
+ let type;
93
+ if (cursor.take('[')) {
94
+ const item = parseTypeRef(cursor);
95
+ cursor.expect(']');
96
+ type = { list: true, item, name: item.name, nonNull: false };
97
+ } else {
98
+ type = { list: false, name: cursor.name(), nonNull: false };
99
+ }
100
+ if (cursor.take('!')) type = { ...type, nonNull: true };
101
+ return type;
102
+ }
103
+
104
+ /** Skips one value of any shape (a default value, a directive argument). */
105
+ function skipValue(cursor) {
106
+ const token = cursor.next();
107
+ if (token.kind !== 'punct') return;
108
+ if (token.value === '$') return void cursor.name();
109
+ const close = token.value === '{' ? '}' : token.value === '[' ? ']' : null;
110
+ if (close === null) return;
111
+ while (!cursor.take(close)) {
112
+ if (token.value === '{') {
113
+ cursor.name();
114
+ cursor.expect(':');
115
+ }
116
+ skipValue(cursor);
117
+ }
118
+ }
119
+
120
+ function skipDirectives(cursor) {
121
+ while (cursor.take('@')) {
122
+ cursor.name();
123
+ if (cursor.take('(')) {
124
+ while (!cursor.take(')')) {
125
+ cursor.name();
126
+ cursor.expect(':');
127
+ skipValue(cursor);
128
+ }
129
+ }
130
+ }
131
+ }
132
+
133
+ function skipDescription(cursor) {
134
+ if (cursor.peek()?.kind === 'string') cursor.next();
135
+ }
136
+
137
+ /** Field or input-value definitions between braces. */
138
+ function parseFieldDefinitions(cursor, withArguments) {
139
+ const fields = new Map();
140
+ cursor.expect('{');
141
+ while (!cursor.take('}')) {
142
+ skipDescription(cursor);
143
+ const name = cursor.name();
144
+ const args = new Map();
145
+ if (withArguments && cursor.take('(')) {
146
+ while (!cursor.take(')')) {
147
+ skipDescription(cursor);
148
+ const argName = cursor.name();
149
+ cursor.expect(':');
150
+ const type = parseTypeRef(cursor);
151
+ const hasDefault = cursor.take('=');
152
+ if (hasDefault) skipValue(cursor);
153
+ skipDirectives(cursor);
154
+ args.set(argName, { type, hasDefault });
155
+ }
156
+ }
157
+ cursor.expect(':');
158
+ const type = parseTypeRef(cursor);
159
+ const hasDefault = cursor.take('=');
160
+ if (hasDefault) skipValue(cursor);
161
+ skipDirectives(cursor);
162
+ fields.set(name, { type, args, hasDefault });
163
+ }
164
+ return fields;
165
+ }
166
+
167
+ /** The types of an SDL source: Map(name -> { kind, fields? , values? , members? }), plus the root operation type names. */
168
+ export function parseSchema(source) {
169
+ const cursor = cursorOf(tokenize(source));
170
+ const types = new Map();
171
+ const roots = { query: 'Query', mutation: 'Mutation', subscription: 'Subscription' };
172
+ while (!cursor.done()) {
173
+ skipDescription(cursor);
174
+ const keyword = cursor.name();
175
+ if (keyword === 'schema' || (keyword === 'extend' && cursor.is('schema'))) {
176
+ if (keyword === 'extend') cursor.name();
177
+ skipDirectives(cursor);
178
+ cursor.expect('{');
179
+ while (!cursor.take('}')) {
180
+ const operation = cursor.name();
181
+ cursor.expect(':');
182
+ roots[operation] = cursor.name();
183
+ }
184
+ } else if (keyword === 'directive') {
185
+ cursor.expect('@');
186
+ cursor.name();
187
+ if (cursor.take('(')) parseArgumentList(cursor);
188
+ if (cursor.is('repeatable')) cursor.name();
189
+ cursor.name();
190
+ cursor.take('|');
191
+ cursor.name();
192
+ while (cursor.take('|')) cursor.name();
193
+ } else if (keyword === 'scalar') {
194
+ const name = cursor.name();
195
+ skipDirectives(cursor);
196
+ types.set(name, { kind: 'scalar' });
197
+ } else if (keyword === 'enum') {
198
+ const name = cursor.name();
199
+ skipDirectives(cursor);
200
+ const values = new Set();
201
+ cursor.expect('{');
202
+ while (!cursor.take('}')) {
203
+ skipDescription(cursor);
204
+ values.add(cursor.name());
205
+ skipDirectives(cursor);
206
+ }
207
+ types.set(name, { kind: 'enum', values });
208
+ } else if (keyword === 'union') {
209
+ const name = cursor.name();
210
+ skipDirectives(cursor);
211
+ cursor.expect('=');
212
+ cursor.take('|');
213
+ const members = [cursor.name()];
214
+ while (cursor.take('|')) members.push(cursor.name());
215
+ types.set(name, { kind: 'union', members });
216
+ } else if (keyword === 'type' || keyword === 'interface' || keyword === 'input') {
217
+ const name = cursor.name();
218
+ if (cursor.is('implements')) {
219
+ cursor.name();
220
+ cursor.take('&');
221
+ cursor.name();
222
+ while (cursor.take('&')) cursor.name();
223
+ }
224
+ skipDirectives(cursor);
225
+ const kind = keyword === 'input' ? 'input' : keyword === 'interface' ? 'interface' : 'object';
226
+ types.set(name, { kind, fields: parseFieldDefinitions(cursor, kind !== 'input') });
227
+ } else {
228
+ throw new SyntaxError(`unexpected "${keyword}" in a schema`);
229
+ }
230
+ }
231
+ return { types, roots };
232
+ }
233
+
234
+ function parseArgumentList(cursor) {
235
+ while (!cursor.take(')')) {
236
+ skipDescription(cursor);
237
+ cursor.name();
238
+ cursor.expect(':');
239
+ parseTypeRef(cursor);
240
+ if (cursor.take('=')) skipValue(cursor);
241
+ skipDirectives(cursor);
242
+ }
243
+ }
244
+
245
+ /** A value of a document: `{ kind: 'variable', name }`, `{ kind: 'object', fields: Map }`, `{ kind: 'list', items }` or a scalar. */
246
+ function parseValue(cursor) {
247
+ if (cursor.take('$')) return { kind: 'variable', name: cursor.name() };
248
+ if (cursor.take('{')) {
249
+ const fields = new Map();
250
+ while (!cursor.take('}')) {
251
+ const name = cursor.name();
252
+ cursor.expect(':');
253
+ fields.set(name, parseValue(cursor));
254
+ }
255
+ return { kind: 'object', fields };
256
+ }
257
+ if (cursor.take('[')) {
258
+ const items = [];
259
+ while (!cursor.take(']')) items.push(parseValue(cursor));
260
+ return { kind: 'list', items };
261
+ }
262
+ const token = cursor.next();
263
+ return { kind: token.kind === 'name' ? 'literal-name' : 'literal', value: token.value };
264
+ }
265
+
266
+ /** A selection set: [{ kind: 'field', name, alias, args: Map, selections } | { kind: 'inline', on, selections } | { kind: 'spread', name }]. */
267
+ function parseSelectionSet(cursor) {
268
+ const selections = [];
269
+ cursor.expect('{');
270
+ while (!cursor.take('}')) {
271
+ if (cursor.peek()?.kind === 'spread') {
272
+ cursor.next();
273
+ if (cursor.is('on')) {
274
+ cursor.name();
275
+ const on = cursor.name();
276
+ skipDirectives(cursor);
277
+ selections.push({ kind: 'inline', on, selections: parseSelectionSet(cursor) });
278
+ } else if (cursor.is('{') || cursor.is('@')) {
279
+ skipDirectives(cursor);
280
+ selections.push({ kind: 'inline', on: null, selections: parseSelectionSet(cursor) });
281
+ } else {
282
+ selections.push({ kind: 'spread', name: cursor.name() });
283
+ skipDirectives(cursor);
284
+ }
285
+ continue;
286
+ }
287
+ let name = cursor.name();
288
+ let alias = null;
289
+ if (cursor.take(':')) {
290
+ alias = name;
291
+ name = cursor.name();
292
+ }
293
+ const args = new Map();
294
+ if (cursor.take('(')) {
295
+ while (!cursor.take(')')) {
296
+ const argName = cursor.name();
297
+ cursor.expect(':');
298
+ args.set(argName, parseValue(cursor));
299
+ }
300
+ }
301
+ skipDirectives(cursor);
302
+ const subSelections = cursor.is('{') ? parseSelectionSet(cursor) : null;
303
+ selections.push({ kind: 'field', name, alias, args, selections: subSelections });
304
+ }
305
+ return selections;
306
+ }
307
+
308
+ /** The operations and fragments of an executable document. */
309
+ export function parseDocument(source) {
310
+ const cursor = cursorOf(tokenize(source));
311
+ const operations = [];
312
+ const fragments = new Map();
313
+ while (!cursor.done()) {
314
+ if (cursor.is('{')) {
315
+ operations.push({ operation: 'query', name: null, variables: new Map(), selections: parseSelectionSet(cursor) });
316
+ continue;
317
+ }
318
+ const keyword = cursor.name();
319
+ if (keyword === 'fragment') {
320
+ const name = cursor.name();
321
+ if (!cursor.take('on')) throw new SyntaxError(`fragment ${name} names no type condition`);
322
+ const on = cursor.name();
323
+ skipDirectives(cursor);
324
+ fragments.set(name, { on, selections: parseSelectionSet(cursor) });
325
+ continue;
326
+ }
327
+ if (keyword !== 'query' && keyword !== 'mutation' && keyword !== 'subscription') throw new SyntaxError(`unexpected "${keyword}" in a document`);
328
+ const name = cursor.peek()?.kind === 'name' ? cursor.name() : null;
329
+ const variables = new Map();
330
+ if (cursor.take('(')) {
331
+ while (!cursor.take(')')) {
332
+ cursor.expect('$');
333
+ const variable = cursor.name();
334
+ cursor.expect(':');
335
+ const type = parseTypeRef(cursor);
336
+ if (cursor.take('=')) skipValue(cursor);
337
+ skipDirectives(cursor);
338
+ variables.set(variable, type);
339
+ }
340
+ }
341
+ skipDirectives(cursor);
342
+ operations.push({ operation: keyword, name, variables, selections: parseSelectionSet(cursor) });
343
+ }
344
+ return { operations, fragments };
345
+ }
346
+
347
+ const typeText = (type) => (type.list ? `[${typeText(type.item)}]` : type.name) + (type.nonNull ? '!' : '');
348
+
349
+ /** The problems of one value against the input type it is passed as. */
350
+ function valueProblems(schema, value, type, where, operation, used) {
351
+ if (value.kind === 'variable') {
352
+ used.add(value.name);
353
+ const declared = operation.variables.get(value.name);
354
+ if (declared === undefined) return [`${where} uses $${value.name}, which the operation does not declare`];
355
+ return declared.name === type.name ? [] : [`${where} takes ${typeText(type)}, but $${value.name} is ${typeText(declared)}`];
356
+ }
357
+ if (type.list && value.kind === 'list') return value.items.flatMap((item, i) => valueProblems(schema, item, type.item, `${where}[${i}]`, operation, used));
358
+ const named = schema.types.get(type.name);
359
+ if (value.kind === 'object') {
360
+ if (named?.kind !== 'input') return [`${where} is an object, but ${type.name} is not an input type`];
361
+ const problems = [];
362
+ for (const [field, inner] of value.fields) {
363
+ const declared = named.fields.get(field);
364
+ if (declared === undefined) problems.push(`${where} names ${field}, which the input ${type.name} does not declare (it declares ${[...named.fields.keys()].join(', ') || 'nothing'})`);
365
+ else problems.push(...valueProblems(schema, inner, declared.type, `${where}.${field}`, operation, used));
366
+ }
367
+ for (const [field, declared] of named.fields) {
368
+ if (declared.type.nonNull && !declared.hasDefault && !value.fields.has(field)) problems.push(`${where} omits ${field}, which the input ${type.name} requires`);
369
+ }
370
+ return problems;
371
+ }
372
+ return [];
373
+ }
374
+
375
+ /** The problems of one selection set against its parent type. */
376
+ function selectionProblems(schema, document, parentName, selections, path, operation, used, visiting = new Set()) {
377
+ const parent = schema.types.get(parentName);
378
+ const problems = [];
379
+ for (const selection of selections) {
380
+ if (selection.kind === 'spread') {
381
+ const fragment = document.fragments.get(selection.name);
382
+ if (fragment === undefined) problems.push(`${path} spreads ...${selection.name}, which the document does not define`);
383
+ else if (!visiting.has(selection.name)) problems.push(...selectionProblems(schema, document, fragment.on, fragment.selections, path, operation, used, new Set([...visiting, selection.name])));
384
+ continue;
385
+ }
386
+ if (selection.kind === 'inline') {
387
+ problems.push(...selectionProblems(schema, document, selection.on ?? parentName, selection.selections, path, operation, used, visiting));
388
+ continue;
389
+ }
390
+ if (selection.name === '__typename') continue;
391
+ const where = `${path}.${selection.name}`;
392
+ const field = parent?.fields?.get(selection.name);
393
+ if (field === undefined) {
394
+ const known = parent?.fields ? [...parent.fields.keys()].join(', ') : 'nothing';
395
+ problems.push(`${where} is not a field of ${parentName} (it has ${known})`);
396
+ continue;
397
+ }
398
+ for (const [arg, value] of selection.args) {
399
+ const declared = field.args.get(arg);
400
+ if (declared === undefined) problems.push(`${where} passes ${arg}, which ${parentName}.${selection.name} does not take (it takes ${[...field.args.keys()].join(', ') || 'no argument'})`);
401
+ else problems.push(...valueProblems(schema, value, declared.type, `${where}(${arg})`, operation, used));
402
+ }
403
+ for (const [arg, declared] of field.args) {
404
+ if (declared.type.nonNull && !declared.hasDefault && !selection.args.has(arg)) problems.push(`${where} omits ${arg}, which ${parentName}.${selection.name} requires`);
405
+ }
406
+ const target = schema.types.get(field.type.name);
407
+ const leaf = BUILT_IN_SCALARS.has(field.type.name) || target?.kind === 'scalar' || target?.kind === 'enum';
408
+ if (leaf && selection.selections !== null) problems.push(`${where} is a ${field.type.name}, which selects no fields`);
409
+ else if (!leaf && selection.selections === null) problems.push(`${where} is a ${field.type.name}, which needs a selection of its fields`);
410
+ else if (!leaf) problems.push(...selectionProblems(schema, document, field.type.name, selection.selections, where, operation, used, visiting));
411
+ }
412
+ return problems;
413
+ }
414
+
415
+ /** The root fields an operation selects (no aliases followed into fragments). */
416
+ export const rootFieldsOf = (operation) => operation.selections.filter((selection) => selection.kind === 'field').map((selection) => selection.name);
417
+
418
+ /** The problems of one operation of a document against one parsed schema; empty when it is valid. */
419
+ export function operationProblems(schema, document, operation) {
420
+ const rootName = schema.roots[operation.operation];
421
+ if (!schema.types.has(rootName)) return [`the contract has no ${operation.operation} type`];
422
+ const used = new Set();
423
+ const label = operation.name ?? `anonymous ${operation.operation}`;
424
+ const problems = selectionProblems(schema, document, rootName, operation.selections, label, operation, used);
425
+ for (const variable of operation.variables.keys()) {
426
+ if (!used.has(variable)) problems.push(`${label} declares $${variable}, which no argument uses`);
427
+ }
428
+ return problems;
429
+ }
@@ -0,0 +1,13 @@
1
+ // is-main.mjs - is this module the process entry point (`node file.mjs`, not an import)? Compared by real path.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import {fileURLToPath} from 'node:url';
5
+
6
+ /** The real path of a file, or its resolved path when it cannot be read (a missing file is never the entry module). */
7
+ const realPathOf = (file) => { try { return fs.realpathSync.native(path.resolve(file)); } catch { return path.resolve(file); } };
8
+ /**
9
+ * True when the module at `metaUrl` is the process entry point. Both sides are compared as REAL paths: Node gives a module
10
+ * its symlink-resolved URL, while argv[1] keeps the path it was started with, so an entry reached through a node_modules
11
+ * junction or symlink (a lane worktree, an npx shim) would otherwise look like an import and silently do nothing.
12
+ */
13
+ export const isMain = (metaUrl, argv = process.argv) => Boolean(argv[1]) && realPathOf(argv[1]) === realPathOf(fileURLToPath(metaUrl));
@@ -1,6 +1,6 @@
1
1
  // language.mjs - the one home of "is this text English?" for source, comments, tests and docs (HFS_LANGUAGE_NOT_ENGLISH,
2
2
  // BE_SOURCE_FORM, FE_SOURCE_FORM). Shared by the architecture machine (docs) and by both lint canons (each ships a byte
3
- // copy in its runtime/ bundle, kept by packages/hfs/scripts/sync-runtime.mjs).
3
+ // copy in its runtime/ bundle, kept by scripts/hfs/sync-runtime.mjs).
4
4
  //
5
5
  // Detection is structural on characters, never a word list: the letters Vietnamese adds to the Latin alphabet (a-breve, a-circumflex,
6
6
  // d-stroke, e-circumflex, o-circumflex, o-horn, u-horn) and every vowel carrying a tone mark. Text is folded to NFC first, so a
@@ -26,7 +26,7 @@ export function secondLanguageHits(text) {
26
26
 
27
27
  /**
28
28
  * The Vietnamese fields of one failure-code entry: the operator text the owner mandated for the failure-code catalog
29
- * (scripts/checks/failure-codes.mjs types the entry with exactly these). causes_vi is the list companion of the three scalar fields.
29
+ * (scripts/checks/check-failure-codes.mjs types the entry with exactly these). causes_vi is the list companion of the three scalar fields.
30
30
  */
31
31
  export const FAILURE_CODE_VIETNAMESE_FIELDS = Object.freeze(['title_vi', 'meaning_vi', 'nextStep_vi', 'causes_vi']);
32
32
 
@@ -15,3 +15,5 @@ export const foldCase = (p) => (WIN ? p.toLowerCase() : p);
15
15
  export const samePath = (a, b) => foldCase(a) === foldCase(b);
16
16
  /** A path's comparable identity: absolute, forward slashes, no trailing slash, case-folded on Windows. */
17
17
  export const pathKey = (p) => foldCase(slash(path.resolve(p)).replace(/\/+$/, ''));
18
+ /** A work-relative path in one spelling: forward slashes, no leading ./, no trailing slash. */
19
+ export const normWork = (p) => String(p ?? '').trim().replaceAll('\\', '/').replace(/^\.\/+/, '').replace(/\/+$/, '');
@@ -1,6 +1,6 @@
1
1
  // stack-declaration.mjs - the one reader of a repository's stack declaration (.starcistacks/application-stacks.yaml): where it
2
- // lives, its bounded read and parse, and the stack block of one service as declared. scripts/checks/check-starcistacks.mjs (the
3
- // services contract) and scripts/lib/hfs-rules/stacks.mjs (the .starcistacks shape, R10) both read it here; this file imports
2
+ // lives, its bounded read and parse, and the stack block of one service as declared. scripts/gates/starcistacks.mjs (the
3
+ // services contract) and scripts/hfs/rules/stacks.mjs (the .starcistacks shape, R10) both read it here; this file imports
4
4
  // nothing of the kernel, so the published @starci/hfs bundle carries it without the ledger.
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
@@ -1,23 +1,12 @@
1
+ // walk.mjs - filesystem walking helpers: isInside (containment) and walkFiles (depth-first file listing).
1
2
  import fs from 'node:fs';
2
3
  import path from 'node:path';
3
- import {fileURLToPath} from 'node:url';
4
4
 
5
5
  /** Whether `target` is `root` itself or inside it (filesystem paths, either separator). */
6
6
  export function isInside(root, target) {
7
7
  const relative = path.relative(root, target);
8
8
  return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
9
9
  }
10
-
11
- /** True when this module is the process entry point (`node file.mjs`, not an import). */
12
- /** The real path of a file, or its resolved path when it cannot be read (a missing file is never the entry module). */
13
- const realPathOf = (file) => { try { return fs.realpathSync.native(path.resolve(file)); } catch { return path.resolve(file); } };
14
- /**
15
- * True when the module at `metaUrl` is the process entry point. Both sides are compared as REAL paths: Node gives a module
16
- * its symlink-resolved URL, while argv[1] keeps the path it was started with, so an entry reached through a node_modules
17
- * junction or symlink (a lane worktree, an npx shim) would otherwise look like an import and silently do nothing.
18
- */
19
- export const isMain = (metaUrl, argv = process.argv) => Boolean(argv[1]) && realPathOf(argv[1]) === realPathOf(fileURLToPath(metaUrl));
20
-
21
10
  /**
22
11
  * Every entry under `dir` that is not a directory, depth-first; `filter` sees the entry name and full
23
12
  * path. `sorted` sorts each directory's entries by name (default: readdir order). `exclude`
package/scaffold/app.mjs CHANGED
@@ -19,7 +19,7 @@
19
19
  import fs from 'node:fs';
20
20
  import path from 'node:path';
21
21
  import { spawnSync } from 'node:child_process';
22
- import { loadSlotManifest, resolveRepoDeclaration } from '../runtime/scripts/lib/hfs-slots.mjs';
22
+ import { loadSlotManifest, resolveRepoDeclaration } from '../runtime/scripts/hfs/slots.mjs';
23
23
  import { parseYaml } from '../runtime/engine/yaml.mjs';
24
24
  import { TEMPLATES_DIR, renderTargets, writeTargets } from '../sync/index.mjs';
25
25
  import { ScaffoldError } from './service.mjs';
@@ -14,8 +14,8 @@
14
14
  import fs from 'node:fs';
15
15
  import path from 'node:path';
16
16
  import { createRequire } from 'node:module';
17
- import { explainPath } from '../runtime/scripts/lib/hfs-check.mjs';
18
- import { loadSlotManifest, readRepoDeclaration } from '../runtime/scripts/lib/hfs-slots.mjs';
17
+ import { explainPath } from '../runtime/scripts/hfs/check.mjs';
18
+ import { loadSlotManifest, readRepoDeclaration } from '../runtime/scripts/hfs/slots.mjs';
19
19
 
20
20
  export class ScaffoldError extends Error {
21
21
  constructor(code, message) {
package/sync/hygiene.mjs CHANGED
@@ -1,21 +1,21 @@
1
1
  // hfs work-hygiene: the guard for the two trees an app tracks besides source. A file under the app root's .starciwork must be
2
2
  // product content (the .starciwork/.gitignore allowlist admits it, so agent output is refused), and a file under the app root's
3
3
  // .starcistacks must not be a plaintext secret (only *.enc is sealed). It is also the secrets guard of the commit: every staged file, in
4
- // any tree, is read from the index and judged with the one secret judgement of `hfs check` (scripts/lib/hfs-rules/secrets.mjs: a secret by
4
+ // any tree, is read from the index and judged with the one secret judgement of `hfs check` (scripts/hfs/rules/secrets.mjs: a secret by
5
5
  // being, an .enc that is no sops envelope, a line that matches a secret pattern), so no plaintext secret reaches the history whatever
6
6
  // .gitignore says (`git add -f`, a path tracked before a rule tightened). There is no override. The pre-commit hook judges the staged
7
- // files; scripts/checks/check-hfs-sync.mjs judges every tracked file.
7
+ // files; scripts/gates/hfs-sync.mjs judges every tracked file.
8
8
  //
9
9
  // Run inside a full runtime checkout — the repository under judgment is the checkout — it also reports the
10
- // state-root ledger findings of that checkout's scripts/lib/hk-orphan-ledgers.mjs: LEDGER_ORPHAN_STATE_ROOT and
10
+ // state-root ledger findings of that checkout's scripts/housekeeping/hk-orphan-ledgers.mjs: LEDGER_ORPHAN_STATE_ROOT and
11
11
  // LEDGER_LEGACY_WORK_SQLITE (COOK-BRIEF F4 handover, incident 2026-09-30). Any other repository — a product repo,
12
- // a bare repo — carries no scripts/checks/ledger-hygiene.mjs at its root, so that section is silently absent:
12
+ // a bare repo — carries no scripts/housekeeping/ledger-hygiene.mjs at its root, so that section is silently absent:
13
13
  // never a crash, never machine-state findings blamed on a repository that does not own them.
14
14
  import { execFileSync } from 'node:child_process';
15
15
  import fs from 'node:fs';
16
16
  import path from 'node:path';
17
17
  import { pathToFileURL } from 'node:url';
18
- import { secretFileFindings } from '../runtime/scripts/lib/hfs-rules/secrets.mjs';
18
+ import { secretFileFindings } from '../runtime/scripts/hfs/rules/secrets.mjs';
19
19
 
20
20
  const PLAINTEXT_NAME = /(^|\/)(\.env(\..*)?|[^/]*\.(pem|key|identity|age))$/;
21
21
  /** The app root's work tree and stack tree, app-relative (hfs work-hygiene runs at the app root). */
@@ -83,14 +83,14 @@ export function judge(cwd, files) {
83
83
  return { checked: files.length, findings: [...findings, ...secretGuardFindings(cwd, files, refused)] };
84
84
  }
85
85
 
86
- // The scripts/checks/ledger-hygiene.mjs of the checkout under judgment: the root git names for `cwd`, which carries
86
+ // The scripts/housekeeping/ledger-hygiene.mjs of the checkout under judgment: the root git names for `cwd`, which carries
87
87
  // that script only when the repository IS a full StarCi runtime checkout (packages/hfs/sync/ sits 3 directories
88
- // under such a root, the same computation packages/hfs/scripts/sync-runtime.mjs uses). A product repository — or a
88
+ // under such a root, the same computation scripts/hfs/sync-runtime.mjs uses). A product repository — or a
89
89
  // bare repo — has no such file, so the section is silently absent wherever this module happens to be installed.
90
90
  const ledgerHygieneScript = cwd => {
91
91
  try {
92
92
  const root = execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8' }).trim();
93
- return path.join(root, 'scripts', 'checks', 'ledger-hygiene.mjs');
93
+ return path.join(root, 'scripts', 'housekeeping', 'ledger-hygiene.mjs');
94
94
  } catch {
95
95
  return null;
96
96
  }