@starci/hfs 4.0.6 → 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 (134) hide show
  1. package/CHANGELOG.md +17 -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 +16 -0
  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/lib/test-secrets.mjs +2 -6
  51. package/runtime/scripts/{checks/common.mjs → lib/walk.mjs} +1 -12
  52. package/scaffold/app.mjs +1 -1
  53. package/scaffold/service.mjs +2 -2
  54. package/sync/hygiene.mjs +8 -8
  55. package/sync/index.mjs +48 -11
  56. package/templates/app/quality-config/codecov.yml +1 -1
  57. package/templates/app/quality-config/sonar-project.properties +1 -1
  58. package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.ts +2 -2
  59. package/templates/fe/skeleton/apps/__app__/next.config.ts +10 -2
  60. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/error.tsx +2 -2
  61. package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/component.tsx +0 -2
  62. package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/index.tsx +1 -7
  63. package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/component.tsx +0 -2
  64. package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/index.tsx +0 -1
  65. package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/component.tsx +0 -4
  66. package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/index.tsx +1 -1
  67. package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/component.tsx +0 -4
  68. package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/index.tsx +1 -1
  69. package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/component.tsx +0 -4
  70. package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/index.tsx +1 -1
  71. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/request.ts +8 -3
  72. package/templates/fe/skeleton/apps/__app__/src/proxy.ts +1 -1
  73. package/runtime/engine/admission.mjs +0 -284
  74. package/runtime/engine/digest.mjs +0 -10
  75. package/runtime/engine/ledger-db.mjs +0 -1245
  76. package/runtime/engine/machine-db.mjs +0 -1565
  77. package/runtime/engine/migrations/machine/0001-init.sql +0 -887
  78. package/runtime/engine/migrations/machine/0002-worktrees-no-workflow-kind.sql +0 -13
  79. package/runtime/engine/migrations/machine/0003-worktrees-workflow-orca.sql +0 -21
  80. package/runtime/engine/migrations/runtime/0001-init.sql +0 -1072
  81. package/runtime/engine/migrations/runtime/0003-usage-unavailable.sql +0 -13
  82. package/runtime/engine/migrations/runtime/0004-attempt-why.sql +0 -43
  83. package/runtime/engine/migrations/runtime/0005-ended-workflow-views.sql +0 -93
  84. package/runtime/scripts/lib/artifact-hold.mjs +0 -89
  85. package/runtime/scripts/lib/artifact-store.mjs +0 -103
  86. package/runtime/scripts/lib/redact.mjs +0 -148
  87. /package/runtime/scripts/{checks → hfs}/architecture/background-unowned.mjs +0 -0
  88. /package/runtime/scripts/{checks → hfs}/architecture/client-reaches-server.mjs +0 -0
  89. /package/runtime/scripts/{checks → hfs}/architecture/clones.mjs +0 -0
  90. /package/runtime/scripts/{checks → hfs}/architecture/config-unread.mjs +0 -0
  91. /package/runtime/scripts/{checks → hfs}/architecture/constructor-deps.mjs +0 -0
  92. /package/runtime/scripts/{checks → hfs}/architecture/contracts.mjs +0 -0
  93. /package/runtime/scripts/{checks → hfs}/architecture/cross-app-duplicate.mjs +0 -0
  94. /package/runtime/scripts/{checks → hfs}/architecture/dead-exports.mjs +0 -0
  95. /package/runtime/scripts/{checks → hfs}/architecture/default-deny.mjs +0 -0
  96. /package/runtime/scripts/{checks → hfs}/architecture/doc-language.mjs +0 -0
  97. /package/runtime/scripts/{checks → hfs}/architecture/entrypoint.mjs +0 -0
  98. /package/runtime/scripts/{checks → hfs}/architecture/error-codes.mjs +0 -0
  99. /package/runtime/scripts/{checks → hfs}/architecture/error-masked.mjs +0 -0
  100. /package/runtime/scripts/{checks → hfs}/architecture/framework-pinned.mjs +0 -0
  101. /package/runtime/scripts/{checks → hfs}/architecture/frontend.mjs +0 -0
  102. /package/runtime/scripts/{checks → hfs}/architecture/hooks-are-hooks.mjs +0 -0
  103. /package/runtime/scripts/{checks → hfs}/architecture/i18n-keys.mjs +0 -0
  104. /package/runtime/scripts/{checks → hfs}/architecture/index.mjs +0 -0
  105. /package/runtime/scripts/{checks → hfs}/architecture/injection-token-exported.mjs +0 -0
  106. /package/runtime/scripts/{checks → hfs}/architecture/machine-ast.mjs +0 -0
  107. /package/runtime/scripts/{checks → hfs}/architecture/module-per-transport.mjs +0 -0
  108. /package/runtime/scripts/{checks → hfs}/architecture/owners.mjs +0 -0
  109. /package/runtime/scripts/{checks → hfs}/architecture/package-shape.mjs +0 -0
  110. /package/runtime/scripts/{checks → hfs}/architecture/reachability.mjs +0 -0
  111. /package/runtime/scripts/{checks → hfs}/architecture/register-once.mjs +0 -0
  112. /package/runtime/scripts/{checks → hfs}/architecture/registration.mjs +0 -0
  113. /package/runtime/scripts/{checks → hfs}/architecture/route-files-thin.mjs +0 -0
  114. /package/runtime/scripts/{checks → hfs}/architecture/schema-owner.mjs +0 -0
  115. /package/runtime/scripts/{checks → hfs}/architecture/source-names.mjs +0 -0
  116. /package/runtime/scripts/{checks → hfs}/architecture/sql-owner.mjs +0 -0
  117. /package/runtime/scripts/{checks → hfs}/architecture/sql-tokens.mjs +0 -0
  118. /package/runtime/scripts/{checks → hfs}/architecture/symbols.mjs +0 -0
  119. /package/runtime/scripts/{checks → hfs}/architecture/tiers.mjs +0 -0
  120. /package/runtime/scripts/{checks → hfs}/architecture/transport-owner.mjs +0 -0
  121. /package/runtime/scripts/{checks → hfs}/architecture/unit-spec-providers.mjs +0 -0
  122. /package/runtime/scripts/{lib → hfs}/repo-identity.mjs +0 -0
  123. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/deps.mjs +0 -0
  124. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/fe-no-tests.mjs +0 -0
  125. /package/runtime/scripts/{lib/hfs-rules/frontend.mjs → hfs/rules/frontend-tree.mjs} +0 -0
  126. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/lint-suppression.mjs +0 -0
  127. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/peer-integrations.mjs +0 -0
  128. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/pipeline.mjs +0 -0
  129. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/proof-commands.mjs +0 -0
  130. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/read.mjs +0 -0
  131. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/repo-local-checks.mjs +0 -0
  132. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/spec-placement.mjs +0 -0
  133. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/test-topology.mjs +0 -0
  134. /package/runtime/scripts/{checks → hfs}/typescript-programs.mjs +0 -0
@@ -0,0 +1,16 @@
1
+ // decrypt.mjs — one `sops` decryption: the custody reads (scripts/api/sops/exec-env.mjs execEnv) and the test
2
+ // secrets (scripts/lib/test-secrets.mjs testSecret). The plaintext stays in this process's memory (stdout over a pipe).
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+
7
+ /**
8
+ * Run `<bin> <args>` with SOPS_AGE_KEY_FILE defaulted to ~/.starci/master.identity. {status, stdout, stderr, error}.
9
+ */
10
+ export function sopsDecrypt(bin, args, { env = process.env, cwd = undefined, maxBuffer = 16 * 1024 * 1024 } = {}) {
11
+ const r = spawnSync(bin, args, {
12
+ cwd, encoding: 'utf8', windowsHide: true, maxBuffer,
13
+ env: { ...env, SOPS_AGE_KEY_FILE: env.SOPS_AGE_KEY_FILE || path.join(os.homedir(), '.starci', 'master.identity') },
14
+ });
15
+ return { status: r.status, stdout: r.stdout, stderr: r.stderr, error: r.error ?? null };
16
+ }
@@ -1,10 +1,10 @@
1
1
  import path from 'node:path';
2
- import { braceVariants, globExpression } from './glob.mjs';
2
+ import { braceVariants, globExpression } from '../lib/glob.mjs';
3
3
 
4
4
  /**
5
5
  * What a slot's `requires`, `allows` and `forbids` entries (knowledge/hfs/slots.yaml) say about a file, read through the
6
- * resolver of scripts/lib/hfs-slots.mjs. The machine checks that judge "this folder holds only what its slot allows"
7
- * (feature-shape, fe-slot-allows) and the front-end lint rules (through `hfs.allows(file)` of scripts/lib/hfs-view.mjs) read it here so no check keeps a path list.
6
+ * resolver of scripts/hfs/slots.mjs. The machine checks that judge "this folder holds only what its slot allows"
7
+ * (feature-shape, fe-slot-allows) and the front-end lint rules (through `hfs.allows(file)` of scripts/hfs/view.mjs) read it here so no check keeps a path list.
8
8
  *
9
9
  * An entry is a name relative to the slot instance root: `<var>` is the instance's bound variable when the slot binds it
10
10
  * (`<feature>`, `<capability>`) and one path segment of any text otherwise (`<action>`), `*` and `?` stay inside a segment,
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { isInside } from './config.mjs';
3
- import { slotAdmitsFile } from '../../lib/hfs-allows.mjs';
3
+ import { slotAdmitsFile } from '../allows.mjs';
4
4
  import { reachableViolation, relativePath, sourceLocation } from './typescript.mjs';
5
5
 
6
6
  const FORBIDDEN_APP_ROLE = /(?:^|\.)(?:service|provider|providers|resolver|controller|handler|repository|entity|use-case|command|query|listener|consumer|processor)\.[cm]?[jt]sx?$/i;
@@ -2,8 +2,8 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { slash } from '../../lib/path-key.mjs';
4
4
  import { readJsonFile as readJson } from '../../lib/json.mjs';
5
- import { isInside } from '../common.mjs';
6
- import { openHfs } from '../../lib/hfs-slots.mjs';
5
+ import { isInside } from '../../lib/walk.mjs';
6
+ import { openHfs } from '../slots.mjs';
7
7
 
8
8
 
9
9
  /** A file's identity: its resolved real path, or the resolved path when it does not exist. */
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { machineKit, pascal, upperSnake } from './machine-ast.mjs';
4
- import { locateDeclaration } from '../../lib/hfs-slots.mjs';
4
+ import { locateDeclaration } from '../slots.mjs';
5
5
 
6
6
  /**
7
7
  * R84 `connection-map` (BE_CONNECTION_DUPLICATE), folding RED01 and RED02. One physical database is one connection, one
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { treeOf } from './required-files.mjs';
4
- import { allowsFile } from '../../lib/hfs-allows.mjs';
4
+ import { allowsFile } from '../allows.mjs';
5
5
 
6
6
  /**
7
7
  * R47 `contract-fixture-guard` (BE_CONTRACT_UNGUARDED). A fake at the network edge serves payload fixtures; a fixture drifts
@@ -1,6 +1,6 @@
1
1
  import { treeOf } from './required-files.mjs';
2
- import { allowsFile } from '../../lib/hfs-allows.mjs';
3
- import { isFeTestPath } from '../../lib/hfs-rules/fe-no-tests.mjs';
2
+ import { allowsFile } from '../allows.mjs';
3
+ import { isFeTestPath } from '../rules/fe-no-tests.mjs';
4
4
 
5
5
  /**
6
6
  * R94 `fe-slot-allows` (FE_SLOT_FILE_ROLE). A front-end slot that owns a whole directory (`fe.route`, `fe.feature`,
@@ -11,7 +11,7 @@ import { isFeTestPath } from '../../lib/hfs-rules/fe-no-tests.mjs';
11
11
  * Files no slot owns are HFS_PATH_NO_SLOT's (`hfs check`), so together every front-end file is placed by exactly one slot and
12
12
  * named by it. A test path (a `.spec.tsx` or `.test.tsx` beside a component, a test directory or test tooling) is FE_NO_TESTS's
13
13
  * (R97, contract-change fe-no-tests): that rule is the one finding of such a file, so this check leaves it alone, as
14
- * HFS_PATH_NO_SLOT does (scripts/lib/hfs-path-findings.mjs).
14
+ * HFS_PATH_NO_SLOT does (scripts/hfs/path-findings.mjs).
15
15
  */
16
16
  export const FE_SLOT_ALLOWS_RULE_IDS = ['FE_SLOT_FILE_ROLE'];
17
17
 
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { treeOf } from './required-files.mjs';
3
- import { allowsFile } from '../../lib/hfs-allows.mjs';
3
+ import { allowsFile } from '../allows.mjs';
4
4
 
5
5
  /**
6
6
  * R29 `feature-shape` (BE_FEATURE_SHAPE). A feature root holds `index.ts`, `<feature>.module.ts`, `application/`,
@@ -3,7 +3,7 @@ import { relativePath } from './typescript.mjs';
3
3
 
4
4
  /**
5
5
  * The file and owner graph the HFS architecture checks share. Every production source file the TypeScript context
6
- * loaded becomes a node classified by the slot manifest (config.hfs is the resolver of scripts/lib/hfs-slots.mjs);
6
+ * loaded becomes a node classified by the slot manifest (config.hfs is the resolver of scripts/hfs/slots.mjs);
7
7
  * every import, re-export and type-only import between two of them becomes an edge. Nothing here judges: the checks
8
8
  * in tiers.mjs, reachability.mjs, dead-exports.mjs, required-files.mjs and clones.mjs read it.
9
9
  *
@@ -2,10 +2,10 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { isIP } from 'node:net';
4
4
  import { gitOutput } from '../../api/git/lib.mjs';
5
- import { repositoryName } from '../../lib/repo-identity.mjs';
5
+ import { repositoryName } from '../repo-identity.mjs';
6
6
  import { braceVariants } from '../../lib/glob.mjs';
7
- import { createSlotResolver, loadSlotManifest, openHfs } from '../../lib/hfs-slots.mjs';
8
- import { isFeTestPath } from '../../lib/hfs-rules/fe-no-tests.mjs';
7
+ import { createSlotResolver, loadSlotManifest, openHfs } from '../slots.mjs';
8
+ import { isFeTestPath } from '../rules/fe-no-tests.mjs';
9
9
 
10
10
  /**
11
11
  * HFS repository-tree check (knowledge/hfs/README.md): every StarCi repository is an
@@ -0,0 +1,96 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createRequire } from 'node:module';
4
+ import { canonical, exactKeys, isInside, slash } from './config.mjs';
5
+
6
+ const CONTRACT_SCHEMA = 'starci/next-data-lifecycle@1';
7
+ const SOURCE = /\.[cm]?[jt]sx?$/i;
8
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
9
+ const BINDING = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/;
10
+ const TOP_KEYS = new Set(['schema', 'swr', 'hooks']);
11
+ const SWR_KEYS = new Set(['package', 'major']);
12
+ const HOOK_KEYS = new Set(['id', 'path', 'export', 'kind', 'resultBinding', 'identities']);
13
+ const IDENTITY_KEYS = new Set(['id', 'binding', 'gatesRequest', 'resource']);
14
+ function relativeSource(repository, value, label) {
15
+ repository = path.resolve(repository);
16
+ if (typeof value !== 'string' || !value.trim() || path.isAbsolute(value)) throw Error(`${label} must be a non-empty repository-relative source path.`);
17
+ const relative = slash(value.trim()).replace(/^\.\//, '');
18
+ if (relative.split('/').includes('..') || !SOURCE.test(relative) || /\.d\.[cm]?[jt]s$/i.test(relative)) {
19
+ throw Error(`${label} must be a production TypeScript or JavaScript source path.`);
20
+ }
21
+ const absolute = path.resolve(repository, ...relative.split('/'));
22
+ if (!isInside(repository, absolute)) throw Error(`${label} must stay inside the repository.`);
23
+ for (let current = absolute; current !== repository; current = path.dirname(current)) {
24
+ const stat = fs.lstatSync(current);
25
+ if (stat.isSymbolicLink()) throw Error(`${label} must not cross a symbolic-link or junction ancestor.`);
26
+ }
27
+ const stat = fs.lstatSync(absolute);
28
+ if (!stat.isFile() || !isInside(canonical(repository), canonical(absolute))) throw Error(`${label} must be a regular source file inside the repository.`);
29
+ return relative;
30
+ }
31
+
32
+ export function parseContract(config) {
33
+ // The contract is declared in the app's one package.json (the app root; config.root is the fe side folder).
34
+ const manifest = JSON.parse(fs.readFileSync(path.join(config.packageRoot ?? config.root, 'package.json'), 'utf8'));
35
+ const next = manifest?.starci?.codePatterns?.next;
36
+ const value = next?.dataLifecycle;
37
+ if (value === undefined) return null;
38
+ if (next.schema !== 'starci/next-code-pattern-contract@1') throw Error('package.json#starci.codePatterns.next must use starci/next-code-pattern-contract@1.');
39
+ exactKeys(value, TOP_KEYS, 'package.json#starci.codePatterns.next.dataLifecycle');
40
+ if (value.schema !== CONTRACT_SCHEMA) throw Error(`dataLifecycle.schema must be ${CONTRACT_SCHEMA}.`);
41
+ exactKeys(value.swr, SWR_KEYS, 'dataLifecycle.swr');
42
+ if (value.swr.package !== 'swr' || !Number.isInteger(value.swr.major) || value.swr.major < 1) {
43
+ throw Error('dataLifecycle.swr must bind package swr and one positive integer major version.');
44
+ }
45
+ if (!Array.isArray(value.hooks) || value.hooks.length === 0) throw Error('dataLifecycle.hooks must contain at least one declared lifecycle call.');
46
+ const ids = new Set();
47
+ const hooks = value.hooks.map((hook, index) => {
48
+ const label = `dataLifecycle.hooks[${index}]`;
49
+ exactKeys(hook, HOOK_KEYS, label);
50
+ if (typeof hook.id !== 'string' || !hook.id.trim() || ids.has(hook.id.trim())) throw Error(`${label}.id must be unique and non-empty.`);
51
+ ids.add(hook.id.trim());
52
+ const source = relativeSource(config.root, hook.path, `${label}.path`);
53
+ if (typeof hook.export !== 'string' || !/^use[A-Z0-9_$][\w$]*$/.test(hook.export)) throw Error(`${label}.export must name one exported use* hook.`);
54
+ if (!['query', 'mutation'].includes(hook.kind)) throw Error(`${label}.kind must be query or mutation.`);
55
+ if (hook.resultBinding !== undefined && (typeof hook.resultBinding !== 'string' || !IDENTIFIER.test(hook.resultBinding))) {
56
+ throw Error(`${label}.resultBinding must be one local identifier.`);
57
+ }
58
+ if (!Array.isArray(hook.identities)) throw Error(`${label}.identities must be an array.`);
59
+ const identityIds = new Set(), bindings = new Set();
60
+ const identities = hook.identities.map((identity, identityIndex) => {
61
+ const identityLabel = `${label}.identities[${identityIndex}]`;
62
+ exactKeys(identity, IDENTITY_KEYS, identityLabel);
63
+ if (typeof identity.id !== 'string' || !identity.id.trim() || identityIds.has(identity.id.trim())) throw Error(`${identityLabel}.id must be unique and non-empty.`);
64
+ if (typeof identity.binding !== 'string' || !BINDING.test(identity.binding) || bindings.has(identity.binding)) throw Error(`${identityLabel}.binding must be a unique identifier or property path.`);
65
+ if (typeof identity.gatesRequest !== 'boolean' || typeof identity.resource !== 'boolean') throw Error(`${identityLabel} must explicitly declare gatesRequest and resource booleans.`);
66
+ identityIds.add(identity.id.trim());
67
+ bindings.add(identity.binding);
68
+ return { id: identity.id.trim(), binding: identity.binding, gatesRequest: identity.gatesRequest, resource: identity.resource };
69
+ });
70
+ if (hook.kind === 'mutation' && !identities.some(identity => identity.resource)) throw Error(`${label} mutation must declare at least one resource identity.`);
71
+ return { id: hook.id.trim(), path: source, export: hook.export, kind: hook.kind,
72
+ ...(hook.resultBinding === undefined ? {} : { resultBinding: hook.resultBinding }), identities };
73
+ });
74
+ return { schema: value.schema, swr: { package: 'swr', major: value.swr.major }, hooks };
75
+ }
76
+
77
+ export function installedSWR(repository) {
78
+ const require = createRequire(path.join(repository, 'package.json'));
79
+ let manifest;
80
+ try { manifest = require.resolve('swr/package.json'); } catch {
81
+ let entry;
82
+ try { entry = require.resolve('swr'); } catch { throw Error('Installed swr package is unavailable.'); }
83
+ let current = path.dirname(entry);
84
+ while (isInside(repository, current) && current !== repository) {
85
+ const candidate = path.join(current, 'package.json');
86
+ if (fs.existsSync(candidate)) { manifest = candidate; break; }
87
+ current = path.dirname(current);
88
+ }
89
+ }
90
+ if (!manifest) throw Error('Installed swr package.json is unavailable.');
91
+ const actualManifest = canonical(manifest);
92
+ const parsed = JSON.parse(fs.readFileSync(actualManifest, 'utf8'));
93
+ const major = Number.parseInt(String(parsed.version ?? '').split('.')[0], 10);
94
+ if (parsed.name !== 'swr' || !Number.isInteger(major)) throw Error('Installed swr package identity or version is invalid.');
95
+ return { name: parsed.name, version: parsed.version, major, root: canonical(path.dirname(actualManifest)) };
96
+ }
@@ -1,107 +1,14 @@
1
- import fs from 'node:fs';
2
1
  import path from 'node:path';
3
- import { createRequire } from 'node:module';
4
- import { canonical, exactKeys, isInside, slash } from './config.mjs';
2
+ import { canonical, isInside } from './config.mjs';
3
+ import { parseContract, installedSWR } from './next-data-contract.mjs';
5
4
  import { relativePath, sourceLocation, unwrapExpression } from './typescript.mjs';
6
5
 
7
6
  export const SWR_KEY_RULE_ID = 'FE_SWR_KEY_IDENTITY';
8
7
  export const SWR_MUTATION_RULE_ID = 'FE_SWR_MUTATION_RESOURCE_IDENTITY';
9
8
  export const SWR_DATA_RULE_IDS = [SWR_KEY_RULE_ID, SWR_MUTATION_RULE_ID];
10
9
 
11
- const CONTRACT_SCHEMA = 'starci/next-data-lifecycle@1';
12
- const SOURCE = /\.[cm]?[jt]sx?$/i;
13
- const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
14
- const BINDING = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/;
15
- const TOP_KEYS = new Set(['schema', 'swr', 'hooks']);
16
- const SWR_KEYS = new Set(['package', 'major']);
17
- const HOOK_KEYS = new Set(['id', 'path', 'export', 'kind', 'resultBinding', 'identities']);
18
- const IDENTITY_KEYS = new Set(['id', 'binding', 'gatesRequest', 'resource']);
19
10
  const SWR_SPECIFIERS = new Set(['swr', 'swr/immutable', 'swr/mutation']);
20
11
 
21
- function relativeSource(repository, value, label) {
22
- repository = path.resolve(repository);
23
- if (typeof value !== 'string' || !value.trim() || path.isAbsolute(value)) throw Error(`${label} must be a non-empty repository-relative source path.`);
24
- const relative = slash(value.trim()).replace(/^\.\//, '');
25
- if (relative.split('/').includes('..') || !SOURCE.test(relative) || /\.d\.[cm]?[jt]s$/i.test(relative)) {
26
- throw Error(`${label} must be a production TypeScript or JavaScript source path.`);
27
- }
28
- const absolute = path.resolve(repository, ...relative.split('/'));
29
- if (!isInside(repository, absolute)) throw Error(`${label} must stay inside the repository.`);
30
- for (let current = absolute; current !== repository; current = path.dirname(current)) {
31
- const stat = fs.lstatSync(current);
32
- if (stat.isSymbolicLink()) throw Error(`${label} must not cross a symbolic-link or junction ancestor.`);
33
- }
34
- const stat = fs.lstatSync(absolute);
35
- if (!stat.isFile() || !isInside(canonical(repository), canonical(absolute))) throw Error(`${label} must be a regular source file inside the repository.`);
36
- return relative;
37
- }
38
-
39
- function parseContract(config) {
40
- // The contract is declared in the app's one package.json (the app root; config.root is the fe side folder).
41
- const manifest = JSON.parse(fs.readFileSync(path.join(config.packageRoot ?? config.root, 'package.json'), 'utf8'));
42
- const next = manifest?.starci?.codePatterns?.next;
43
- const value = next?.dataLifecycle;
44
- if (value === undefined) return null;
45
- if (next.schema !== 'starci/next-code-pattern-contract@1') throw Error('package.json#starci.codePatterns.next must use starci/next-code-pattern-contract@1.');
46
- exactKeys(value, TOP_KEYS, 'package.json#starci.codePatterns.next.dataLifecycle');
47
- if (value.schema !== CONTRACT_SCHEMA) throw Error(`dataLifecycle.schema must be ${CONTRACT_SCHEMA}.`);
48
- exactKeys(value.swr, SWR_KEYS, 'dataLifecycle.swr');
49
- if (value.swr.package !== 'swr' || !Number.isInteger(value.swr.major) || value.swr.major < 1) {
50
- throw Error('dataLifecycle.swr must bind package swr and one positive integer major version.');
51
- }
52
- if (!Array.isArray(value.hooks) || value.hooks.length === 0) throw Error('dataLifecycle.hooks must contain at least one declared lifecycle call.');
53
- const ids = new Set();
54
- const hooks = value.hooks.map((hook, index) => {
55
- const label = `dataLifecycle.hooks[${index}]`;
56
- exactKeys(hook, HOOK_KEYS, label);
57
- if (typeof hook.id !== 'string' || !hook.id.trim() || ids.has(hook.id.trim())) throw Error(`${label}.id must be unique and non-empty.`);
58
- ids.add(hook.id.trim());
59
- const source = relativeSource(config.root, hook.path, `${label}.path`);
60
- if (typeof hook.export !== 'string' || !/^use[A-Z0-9_$][\w$]*$/.test(hook.export)) throw Error(`${label}.export must name one exported use* hook.`);
61
- if (!['query', 'mutation'].includes(hook.kind)) throw Error(`${label}.kind must be query or mutation.`);
62
- if (hook.resultBinding !== undefined && (typeof hook.resultBinding !== 'string' || !IDENTIFIER.test(hook.resultBinding))) {
63
- throw Error(`${label}.resultBinding must be one local identifier.`);
64
- }
65
- if (!Array.isArray(hook.identities)) throw Error(`${label}.identities must be an array.`);
66
- const identityIds = new Set(), bindings = new Set();
67
- const identities = hook.identities.map((identity, identityIndex) => {
68
- const identityLabel = `${label}.identities[${identityIndex}]`;
69
- exactKeys(identity, IDENTITY_KEYS, identityLabel);
70
- if (typeof identity.id !== 'string' || !identity.id.trim() || identityIds.has(identity.id.trim())) throw Error(`${identityLabel}.id must be unique and non-empty.`);
71
- if (typeof identity.binding !== 'string' || !BINDING.test(identity.binding) || bindings.has(identity.binding)) throw Error(`${identityLabel}.binding must be a unique identifier or property path.`);
72
- if (typeof identity.gatesRequest !== 'boolean' || typeof identity.resource !== 'boolean') throw Error(`${identityLabel} must explicitly declare gatesRequest and resource booleans.`);
73
- identityIds.add(identity.id.trim());
74
- bindings.add(identity.binding);
75
- return { id: identity.id.trim(), binding: identity.binding, gatesRequest: identity.gatesRequest, resource: identity.resource };
76
- });
77
- if (hook.kind === 'mutation' && !identities.some(identity => identity.resource)) throw Error(`${label} mutation must declare at least one resource identity.`);
78
- return { id: hook.id.trim(), path: source, export: hook.export, kind: hook.kind,
79
- ...(hook.resultBinding === undefined ? {} : { resultBinding: hook.resultBinding }), identities };
80
- });
81
- return { schema: value.schema, swr: { package: 'swr', major: value.swr.major }, hooks };
82
- }
83
-
84
- function installedSWR(repository) {
85
- const require = createRequire(path.join(repository, 'package.json'));
86
- let manifest;
87
- try { manifest = require.resolve('swr/package.json'); } catch {
88
- let entry;
89
- try { entry = require.resolve('swr'); } catch { throw Error('Installed swr package is unavailable.'); }
90
- let current = path.dirname(entry);
91
- while (isInside(repository, current) && current !== repository) {
92
- const candidate = path.join(current, 'package.json');
93
- if (fs.existsSync(candidate)) { manifest = candidate; break; }
94
- current = path.dirname(current);
95
- }
96
- }
97
- if (!manifest) throw Error('Installed swr package.json is unavailable.');
98
- const actualManifest = canonical(manifest);
99
- const parsed = JSON.parse(fs.readFileSync(actualManifest, 'utf8'));
100
- const major = Number.parseInt(String(parsed.version ?? '').split('.')[0], 10);
101
- if (parsed.name !== 'swr' || !Number.isInteger(major)) throw Error('Installed swr package identity or version is invalid.');
102
- return { name: parsed.name, version: parsed.version, major, root: canonical(path.dirname(actualManifest)) };
103
- }
104
-
105
12
  function normalizedSymbol(ts, checker, value) {
106
13
  let symbol = value ?? null;
107
14
  const seen = new Set();
@@ -543,22 +450,29 @@ function nullish(ts, checker, expression) {
543
450
  return !symbol || (symbol.getDeclarations?.() ?? []).every(declaration => declaration.getSourceFile().isDeclarationFile);
544
451
  }
545
452
 
546
- function trueMeansAvailable(ts, checker, condition, identity) {
453
+ /** True when taking `branch` of `condition` proves the declared identity is available. */
454
+ function branchMeansAvailable(ts, checker, condition, identity, branch) {
547
455
  condition = unwrapExpression(ts, condition);
548
456
  if (expressionReferences(ts, checker, condition, identity) && (ts.isIdentifier(condition)
549
- || ts.isPropertyAccessExpression(condition) || ts.isElementAccessExpression(condition))) return true;
457
+ || ts.isPropertyAccessExpression(condition) || ts.isElementAccessExpression(condition))) return branch;
550
458
  if (ts.isPrefixUnaryExpression(condition) && condition.operator === ts.SyntaxKind.ExclamationToken) {
551
- const nested = trueMeansAvailable(ts, checker, condition.operand, identity);
552
- return nested === null ? null : !nested;
459
+ return branchMeansAvailable(ts, checker, condition.operand, identity, !branch);
553
460
  }
554
461
  if (ts.isBinaryExpression(condition)) {
462
+ const operator = condition.operatorToken.kind;
463
+ if (operator === ts.SyntaxKind.AmpersandAmpersandToken || operator === ts.SyntaxKind.BarBarToken) {
464
+ // `a && b` is true only when both sides are true; `a || b` is false only when both sides are false.
465
+ const decisive = operator === ts.SyntaxKind.AmpersandAmpersandToken ? branch : !branch;
466
+ return decisive && [condition.left, condition.right]
467
+ .some(side => branchMeansAvailable(ts, checker, side, identity, branch));
468
+ }
555
469
  const leftIdentity = expressionReferences(ts, checker, condition.left, identity) && nullish(ts, checker, condition.right);
556
470
  const rightIdentity = expressionReferences(ts, checker, condition.right, identity) && nullish(ts, checker, condition.left);
557
- if (!leftIdentity && !rightIdentity) return null;
558
- if ([ts.SyntaxKind.ExclamationEqualsToken, ts.SyntaxKind.ExclamationEqualsEqualsToken].includes(condition.operatorToken.kind)) return true;
559
- if ([ts.SyntaxKind.EqualsEqualsToken, ts.SyntaxKind.EqualsEqualsEqualsToken].includes(condition.operatorToken.kind)) return false;
471
+ if (!leftIdentity && !rightIdentity) return false;
472
+ if ([ts.SyntaxKind.ExclamationEqualsToken, ts.SyntaxKind.ExclamationEqualsEqualsToken].includes(operator)) return branch;
473
+ if ([ts.SyntaxKind.EqualsEqualsToken, ts.SyntaxKind.EqualsEqualsEqualsToken].includes(operator)) return !branch;
560
474
  }
561
- return null;
475
+ return false;
562
476
  }
563
477
 
564
478
  function violation(config, call, ruleId, message, extra = {}) {
@@ -687,8 +601,7 @@ function inspectKey(config, context, entry, call, identities, violations, reason
687
601
  }
688
602
  if (identity.gatesRequest) {
689
603
  const gated = active.every(leaf => leaf.decisions.some(decision => {
690
- const available = trueMeansAvailable(context.ts, call.checker, decision.condition, identity);
691
- return available !== null && decision.branch === available;
604
+ return branchMeansAvailable(context.ts, call.checker, decision.condition, identity, decision.branch);
692
605
  }));
693
606
  if (!gated) violations.push(violation(config, call, SWR_KEY_RULE_ID,
694
607
  `${entry.id} must produce an explicit null key when ${identity.id} (${identity.binding}) is unavailable.`,
@@ -5,7 +5,7 @@ import { gitOutput } from '../../api/git/lib.mjs';
5
5
  /**
6
6
  * HFS check 5: the files and directories the slot manifest requires (knowledge/hfs/slots.yaml `requires`,
7
7
  * `requiredInstances`, `minInstances`, required app kinds) must exist in the repository tree. Everything is read through the
8
- * resolver of scripts/lib/hfs-slots.mjs; nothing here names a path.
8
+ * resolver of scripts/hfs/slots.mjs; nothing here names a path.
9
9
  *
10
10
  * BE_REQUIRED_MODULE_MISSING a backend app, feature, domain, integrations or platform instance lacks a required file or
11
11
  * directory, or a required platform instance (config, logging, errors, primitives) is absent
@@ -60,7 +60,7 @@ export const LINT_ENFORCERS = Object.freeze([
60
60
  enforcer('component-purity', FE, ['FE_COMPONENT_WORLD_OWNERSHIP', 'FE_WORLD_OWNER_RENDER_BOUNDARY', 'FE_CONNECTED_BLOCK_RENDER_PAIR', 'FE_PURE_REACHES_DATA', 'FE_PURE_WORLD_HOOK', 'FE_PURE_WORLD_IMPORT'], 'A pure component reaches no data or world state; its connected entry owns them.'),
61
61
  enforcer('grammar-entry', FE, ['ARCH_GRAMMAR_CONTRACT_INVALID', 'ARCH_GRAMMAR_EXPORT_BYPASS'], 'Vendor grammar is reached only through its owner entry.'),
62
62
  enforcer('i18n-keys', FE, ['FE_I18N_KEYS'], 'Translation keys read by source and keys held by catalogs agree both ways.'),
63
- // The per-path judgements of the slot manifest (scripts/lib/hfs-path-findings.mjs), on a tracked TypeScript file.
63
+ // The per-path judgements of the slot manifest (scripts/hfs/path-findings.mjs), on a tracked TypeScript file.
64
64
  enforcer('slot-undeclared', BOTH, ['HFS_SLOT_UNDECLARED', 'HFS_SLOT_AMBIGUOUS', 'HFS_SLOT_NOT_ENABLED'], 'A TypeScript file is owned by exactly one slot of the repository.', { origin: 'repo' }),
65
65
  enforcer('source-suffix', BE, ['BE_SOURCE_FORM'], 'A source file name is index.ts, main.ts, a migration or <kebab-name>.<suffix>.ts with a suffix of the closed list.', { origin: 'repo' }),
66
66
  enforcer('spec-placement', BE, ['BE_SPEC_PLACEMENT'], 'A spec file lives in one of the four test layers and nowhere else.', { origin: 'repo' }),
@@ -1,6 +1,6 @@
1
1
  import { treeOf } from './required-files.mjs';
2
- import { allowsFile } from '../../lib/hfs-allows.mjs';
3
- import { locateDeclaration } from '../../lib/hfs-slots.mjs';
2
+ import { allowsFile } from '../allows.mjs';
3
+ import { locateDeclaration } from '../slots.mjs';
4
4
  import { DEFAULT_ENVIRONMENT, STACKS_DIRECTORY, STATEFUL_KINDS, namesOfService, readStack } from '../../lib/stack-services.mjs';
5
5
 
6
6
  /**
@@ -5,7 +5,7 @@ import { canonical, isInside, slash } from './config.mjs';
5
5
  import { sameOrUnder } from '../../lib/path-key.mjs';
6
6
  import { createTypeScriptProgram, readTypeScriptProject, resolveTypeScriptModule, sharedInProgramRun, typeScriptProjectReferencePath } from '../typescript-programs.mjs';
7
7
  import { readJsonFile } from '../../lib/json.mjs';
8
- import { locateDeclaration } from '../../lib/hfs-slots.mjs';
8
+ import { locateDeclaration } from '../slots.mjs';
9
9
 
10
10
  const CODE_EXTENSIONS = /\.(?:[cm]?[jt]sx?)$/i;
11
11
  const TEST_FILE = /(?:^|[.-])(?:spec|test)\.[cm]?[jt]sx?$/i;
@@ -1,13 +1,13 @@
1
1
  // architecture.mjs — the architecture check (docs/architecture-check.md).
2
2
  //
3
- // node scripts/checks/architecture.mjs <repo-root> [--base <commit>]
3
+ // node scripts/hfs/architecture.mjs <repo-root> [--base <commit>]
4
4
  //
5
5
  // Prints one starci/architecture-check@1 record. Exit 0: ok. Exit 1: violations or errors (a check that
6
6
  // cannot run is an error, never a pass). Exit 2: bad arguments.
7
7
  import path from 'node:path';
8
8
  import {fileURLToPath} from 'node:url';
9
9
  import {checkArchitecture} from './architecture/index.mjs';
10
- import { isMain } from './common.mjs';
10
+ import { isMain } from '../lib/is-main.mjs';
11
11
 
12
12
  export {checkArchitecture};
13
13
 
@@ -1,6 +1,6 @@
1
1
  // hfs-check.mjs - the HFS app check, behind `hfs check | explain` (packages/hfs) and reusable by any
2
2
  // runtime check. It reads three things and nothing else: the app's hfs.json, the slot manifest
3
- // (knowledge/hfs/slots.yaml through scripts/lib/hfs-slots.mjs) and the pins (knowledge/hfs/canon-pins.yaml). It never
3
+ // (knowledge/hfs/slots.yaml through scripts/hfs/slots.mjs) and the pins (knowledge/hfs/canon-pins.yaml). It never
4
4
  // writes to the repository it inspects.
5
5
  //
6
6
  // checkRepo() answers, for the tracked paths of one app (git ls-files), per scope (the app root and each side folder):
@@ -28,6 +28,7 @@
28
28
  // HFS_PROOF_COMMAND_FILE_MISSING (R105, hfs-rules/proof-commands.mjs) a .starciwork proof command that runs a file the repository does not hold
29
29
  // HFS_PEER_INTEGRATION_MISSING (R111, hfs-rules/peer-integrations.mjs) the app root package.json lacks the runtime peer a driver integration needs
30
30
  // BE_INTEGRATION_SPEC_MISSING (R112, hfs-rules/integration-specs.mjs) an integration with no integration spec that registers its module, maps its refusals and drives an outage
31
+ // FE_GRAPHQL_CONTRACT (R113, hfs-rules/fe-contract-documents.mjs) a front-end GraphQL document the back end's contract snapshot does not serve
31
32
  // FE_WIRE_GENERATED, FE_I18N_PLACEMENT, FE_I18N_CATALOG (R52, R59, R60, hfs-rules/frontend.mjs) the front-end tree of each app
32
33
  // HFS_GITIGNORE_BLOCK_DRIFT, HFS_SONAR_CONFIG (R04, R11) produced by packages/hfs/sync/managed.mjs, which renders the templates
33
34
  // HFS_FORMAT (R19) produced by packages/hfs/sync/format.mjs, which runs the repository's own prettier
@@ -38,37 +39,41 @@
38
39
  // HFS_EMPTY_DIR a directory with no file below it (git never tracks one), outside .git, node_modules and ignored slots
39
40
  // HFS_GHOST_TREE an empty directory beside a sibling within two edits of its name (business / bussiness)
40
41
  // HFS_UNTRACKED_ROOT_ENTRY an entry git neither tracks nor ignores, outside an `ignored` slot (R03)
41
- // checkRepository() is the whole `hfs check`: checkRepo() plus the architecture machine (scripts/checks/architecture.mjs, one
42
+ // A runtime repository (hfs.json kind runtime, judged with knowledge/hfs/runtime-slots.yaml) gets the per-path slot findings,
43
+ // the required files and minimum instances and the tree findings only; scripts/hfs/runtime-check.mjs adds the runtime rules.
44
+ // checkRepository() is the whole `hfs check`: checkRepo() plus the architecture machine (scripts/hfs/architecture.mjs, one
42
45
  // implementation; the published bundle carries a byte copy), its violations and errors reported as findings under their own
43
46
  // codes; `fast` limits both to the owners changed since the merge-base. Every finding carries its code and the Vietnamese why text of modules/kernel/failure-codes.yaml. Only `error`
44
47
  // findings fail the check.
45
48
  import fs from 'node:fs';
46
49
  import path from 'node:path';
47
50
  import { skillRoot } from '../../engine/runtime-root.mjs';
48
- import { ARCHITECTURE_RULE_IDS, checkArchitecture } from '../checks/architecture/index.mjs';
51
+ import { ARCHITECTURE_RULE_IDS, checkArchitecture } from './architecture/index.mjs';
49
52
  import { parseYaml } from '../../engine/yaml.mjs';
50
- import { APP_SCOPE, HFS_DECLARATION_FILE, appRelativeMessages, HfsSlotsError, SIDES, createSlotResolver, loadSlotManifest, readRepoDeclaration, resolveRepoDeclaration } from './hfs-slots.mjs';
51
- import { allowsFile } from './hfs-allows.mjs';
53
+ import { APP_SCOPE, HFS_DECLARATION_FILE, appRelativeMessages, HfsSlotsError, SIDES, createSlotResolver, loadSlotManifest, readRepoDeclaration, resolveRepoDeclaration } from './slots.mjs';
54
+ import { allowsFile } from './allows.mjs';
55
+ import { RUNTIME_KIND } from './manifest-shape.mjs';
52
56
  import { gitOutput } from '../api/git/lib.mjs';
53
- import { posixPath } from './path-key.mjs';
54
- import { readTree, treeFacts, untrackedEntries } from './hfs-tree.mjs';
55
- import { contractFindings } from './hfs-rules/contract.mjs';
56
- import { depFindings } from './hfs-rules/deps.mjs';
57
- import { appFrontendFindings, frontendFindings } from './hfs-rules/frontend.mjs';
58
- import { integrationSpecFindings } from './hfs-rules/integration-specs.mjs';
59
- import { lintSuppressionFindings } from './hfs-rules/lint-suppression.mjs';
60
- import { peerIntegrationFindings } from './hfs-rules/peer-integrations.mjs';
61
- import { pipelineFindings } from './hfs-rules/pipeline.mjs';
62
- import { proofCommandFindings } from './hfs-rules/proof-commands.mjs';
63
- import { repoLocalCheckFindings } from './hfs-rules/repo-local-checks.mjs';
64
- import { readJson } from './hfs-rules/read.mjs';
65
- import { secretFindings } from './hfs-rules/secrets.mjs';
66
- import { pathFindings } from './hfs-path-findings.mjs';
67
- import { onLintSurface } from '../checks/architecture/surface.mjs';
68
- import { checkAppRoot, trackedTreeView } from '../checks/architecture/hfs.mjs';
69
- import { stacksFindings } from './hfs-rules/stacks.mjs';
70
- import { testTopologyFindings } from './hfs-rules/test-topology.mjs';
71
- import { feNoTestsFindings, isFeTestPath } from './hfs-rules/fe-no-tests.mjs';
57
+ import { posixPath } from '../lib/path-key.mjs';
58
+ import { readTree, treeFacts, untrackedEntries } from './tree.mjs';
59
+ import { contractFindings } from './rules/contract.mjs';
60
+ import { depFindings } from './rules/deps.mjs';
61
+ import { appFrontendFindings, frontendFindings } from './rules/frontend-tree.mjs';
62
+ import { feContractFindings } from './rules/fe-contract-documents.mjs';
63
+ import { integrationSpecFindings } from './rules/integration-specs.mjs';
64
+ import { lintSuppressionFindings } from './rules/lint-suppression.mjs';
65
+ import { peerIntegrationFindings } from './rules/peer-integrations.mjs';
66
+ import { pipelineFindings } from './rules/pipeline.mjs';
67
+ import { proofCommandFindings } from './rules/proof-commands.mjs';
68
+ import { repoLocalCheckFindings } from './rules/repo-local-checks.mjs';
69
+ import { readJson } from './rules/read.mjs';
70
+ import { secretFindings } from './rules/secrets.mjs';
71
+ import { pathFindings } from './path-findings.mjs';
72
+ import { onLintSurface } from './architecture/surface.mjs';
73
+ import { checkAppRoot, trackedTreeView } from './architecture/hfs.mjs';
74
+ import { stacksFindings } from './rules/stacks.mjs';
75
+ import { testTopologyFindings } from './rules/test-topology.mjs';
76
+ import { feNoTestsFindings, isFeTestPath } from './rules/fe-no-tests.mjs';
72
77
 
73
78
  export const CANON_PINS_FILE = 'knowledge/hfs/canon-pins.yaml';
74
79
  export const FAILURE_CODES_FILE = 'modules/kernel/failure-codes.yaml';
@@ -86,7 +91,7 @@ export const CHECK_CODES = Object.freeze([
86
91
  'HFS_SLOT_REQUIRED_MISSING', 'HFS_MIN_INSTANCES', 'HFS_CANON_PIN_DRIFT', 'HFS_SIZE_SOFT_BACKLOG', 'BE_SOURCE_FORM',
87
92
  'HFS_MANAGED_FILE_DRIFT', 'HFS_TOOL_CONFIG_LOCAL', 'HFS_RULE_OFF_WITHOUT_REPLACEMENT', 'HFS_TS_STRICT',
88
93
  'HFS_PLAINTEXT_SECRET', 'HFS_STACKS_SHAPE', 'HFS_CI_MISSING_CANON', 'HFS_DEP_VERSION_SKEW', 'HFS_CONTRACT_SNAPSHOT_DRIFT',
89
- 'BE_TEST_TOPOLOGY', 'BE_SPEC_PLACEMENT', 'HFS_REPO_LOCAL_CHECK', 'HFS_LINT_SUPPRESSION_FILE', 'HFS_PROOF_COMMAND_FILE_MISSING', 'HFS_PEER_INTEGRATION_MISSING', 'BE_INTEGRATION_SPEC_MISSING', 'FE_NO_TESTS', 'FE_WIRE_GENERATED', 'FE_I18N_PLACEMENT', 'FE_I18N_CATALOG',
94
+ 'BE_TEST_TOPOLOGY', 'BE_SPEC_PLACEMENT', 'HFS_REPO_LOCAL_CHECK', 'HFS_LINT_SUPPRESSION_FILE', 'HFS_PROOF_COMMAND_FILE_MISSING', 'HFS_PEER_INTEGRATION_MISSING', 'BE_INTEGRATION_SPEC_MISSING', 'FE_GRAPHQL_CONTRACT', 'FE_NO_TESTS', 'FE_WIRE_GENERATED', 'FE_I18N_PLACEMENT', 'FE_I18N_CATALOG',
90
95
  'HFS_GITIGNORE_BLOCK_DRIFT', 'HFS_SONAR_CONFIG', 'HFS_FORMAT',
91
96
  'HFS_EMPTY_DIR', 'HFS_GHOST_TREE', 'HFS_UNTRACKED_ROOT_ENTRY',
92
97
  ...REFUSAL_CODES,
@@ -253,6 +258,8 @@ function scopeFindings({ repoRoot, root, repo, resolver, files, all = files, sco
253
258
  const count = instances.filter((i) => i.slot === slot).length;
254
259
  if (count < min) findings.push({ code: 'HFS_MIN_INSTANCES', level: 'error', path: resolver.slot(slot).path, slot, min, count, message: `${slot} needs at least ${min} instance${min === 1 ? '' : 's'} (${resolver.slot(slot).path}), found ${count}` });
255
260
  }
261
+ // The runtime repository (kind runtime) is judged here by its tree law only; its own rules run in scripts/hfs/runtime-check.mjs.
262
+ if (repo.kind === RUNTIME_KIND) return findings;
256
263
 
257
264
  const pins = readPins(root);
258
265
  findings.push(...pinFindings({ repoRoot, files, profile: repo.profile, pins, only: scoped }));
@@ -268,6 +275,7 @@ function scopeFindings({ repoRoot, root, repo, resolver, files, all = files, sco
268
275
  ...depFindings({ repoRoot, files: all }),
269
276
  ...peerIntegrationFindings({ repoRoot, files: all }),
270
277
  ...integrationSpecFindings({ repoRoot, files: all }),
278
+ ...feContractFindings({ repoRoot, files: all }),
271
279
  ...pipelineFindings({ repoRoot, files, pins }),
272
280
  ...testTopologyFindings({ repoRoot, files }),
273
281
  ...proofCommandFindings({ repoRoot, files: all, resolver, sides: Object.keys(repo.sides ?? {}) }),