@starci/eslint-canon-fe 8.0.7 → 8.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 (86) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/docs/grammar-boundary.md +1 -1
  3. package/file-layout.mjs +3 -1
  4. package/grammar-boundary.mjs +4 -4
  5. package/lib/hfs.mjs +3 -3
  6. package/lib/next.mjs +1 -1
  7. package/package.json +3 -3
  8. package/project-graph.mjs +4 -4
  9. package/props-and-slots.mjs +4 -3
  10. package/runtime/engine/runtime-root.mjs +5 -0
  11. package/runtime/engine/yaml.mjs +3 -3
  12. package/runtime/knowledge/hfs/canon-pins.yaml +10 -10
  13. package/runtime/knowledge/hfs/slots.yaml +5 -5
  14. package/runtime/knowledge/patterns/fe/folder.yaml +2 -2
  15. package/runtime/knowledge/sonar-gate.yaml +2 -2
  16. package/runtime/scripts/api/git/lib.mjs +1 -1
  17. package/runtime/scripts/api/git/recorded-lines.mjs +19 -0
  18. package/runtime/scripts/api/sops/decrypt.mjs +1 -1
  19. package/runtime/scripts/{lib/hfs-allows.mjs → hfs/allows.mjs} +3 -3
  20. package/runtime/scripts/{checks → hfs}/architecture/backend.mjs +1 -1
  21. package/runtime/scripts/{checks → hfs}/architecture/config.mjs +2 -2
  22. package/runtime/scripts/{checks → hfs}/architecture/connection-map.mjs +1 -1
  23. package/runtime/scripts/{checks → hfs}/architecture/contract-fixture-guard.mjs +1 -1
  24. package/runtime/scripts/{checks → hfs}/architecture/fe-slot-allows.mjs +3 -3
  25. package/runtime/scripts/{checks → hfs}/architecture/feature-shape.mjs +1 -1
  26. package/runtime/scripts/{checks → hfs}/architecture/hfs-graph.mjs +1 -1
  27. package/runtime/scripts/{checks → hfs}/architecture/hfs.mjs +3 -3
  28. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +96 -0
  29. package/runtime/scripts/{checks → hfs}/architecture/next-data.mjs +18 -105
  30. package/runtime/scripts/{checks → hfs}/architecture/required-files.mjs +1 -1
  31. package/runtime/scripts/{checks → hfs}/architecture/surface.mjs +1 -1
  32. package/runtime/scripts/{checks → hfs}/architecture/test-world-files.mjs +2 -2
  33. package/runtime/scripts/{checks → hfs}/architecture/typescript.mjs +1 -1
  34. package/runtime/scripts/hfs/manifest-shape.mjs +170 -0
  35. package/runtime/scripts/{lib/hfs-path-findings.mjs → hfs/path-findings.mjs} +6 -6
  36. package/runtime/scripts/{lib → hfs}/project-graph.mjs +0 -0
  37. package/runtime/scripts/{lib → hfs}/project-rule.mjs +3 -3
  38. package/runtime/scripts/{lib/hfs-rules → hfs/rules}/secrets.mjs +2 -2
  39. package/runtime/scripts/{lib/hfs-slots.mjs → hfs/slots.mjs} +52 -58
  40. package/runtime/scripts/{lib/hfs-view.mjs → hfs/view.mjs} +3 -3
  41. package/runtime/scripts/lib/language.mjs +2 -2
  42. package/runtime/scripts/lib/line-count.mjs +9 -0
  43. package/runtime/scripts/lib/path-key.mjs +2 -0
  44. package/runtime/scripts/{checks/common.mjs → lib/walk.mjs} +1 -12
  45. package/size-growth.mjs +2 -1
  46. package/runtime/scripts/lib/recorded-lines.mjs +0 -27
  47. /package/runtime/scripts/{checks → hfs}/architecture/background-unowned.mjs +0 -0
  48. /package/runtime/scripts/{checks → hfs}/architecture/client-reaches-server.mjs +0 -0
  49. /package/runtime/scripts/{checks → hfs}/architecture/clones.mjs +0 -0
  50. /package/runtime/scripts/{checks → hfs}/architecture/config-unread.mjs +0 -0
  51. /package/runtime/scripts/{checks → hfs}/architecture/constructor-deps.mjs +0 -0
  52. /package/runtime/scripts/{checks → hfs}/architecture/contracts.mjs +0 -0
  53. /package/runtime/scripts/{checks → hfs}/architecture/cross-app-duplicate.mjs +0 -0
  54. /package/runtime/scripts/{checks → hfs}/architecture/dead-exports.mjs +0 -0
  55. /package/runtime/scripts/{checks → hfs}/architecture/default-deny.mjs +0 -0
  56. /package/runtime/scripts/{checks → hfs}/architecture/doc-language.mjs +0 -0
  57. /package/runtime/scripts/{checks → hfs}/architecture/entrypoint.mjs +0 -0
  58. /package/runtime/scripts/{checks → hfs}/architecture/error-codes.mjs +0 -0
  59. /package/runtime/scripts/{checks → hfs}/architecture/error-masked.mjs +0 -0
  60. /package/runtime/scripts/{checks → hfs}/architecture/framework-pinned.mjs +0 -0
  61. /package/runtime/scripts/{checks → hfs}/architecture/frontend.mjs +0 -0
  62. /package/runtime/scripts/{checks → hfs}/architecture/hooks-are-hooks.mjs +0 -0
  63. /package/runtime/scripts/{checks → hfs}/architecture/i18n-keys.mjs +0 -0
  64. /package/runtime/scripts/{checks → hfs}/architecture/index.mjs +0 -0
  65. /package/runtime/scripts/{checks → hfs}/architecture/injection-token-exported.mjs +0 -0
  66. /package/runtime/scripts/{checks → hfs}/architecture/machine-ast.mjs +0 -0
  67. /package/runtime/scripts/{checks → hfs}/architecture/module-per-transport.mjs +0 -0
  68. /package/runtime/scripts/{checks → hfs}/architecture/owners.mjs +0 -0
  69. /package/runtime/scripts/{checks → hfs}/architecture/package-shape.mjs +0 -0
  70. /package/runtime/scripts/{checks → hfs}/architecture/reachability.mjs +0 -0
  71. /package/runtime/scripts/{checks → hfs}/architecture/register-once.mjs +0 -0
  72. /package/runtime/scripts/{checks → hfs}/architecture/registration.mjs +0 -0
  73. /package/runtime/scripts/{checks → hfs}/architecture/route-files-thin.mjs +0 -0
  74. /package/runtime/scripts/{checks → hfs}/architecture/schema-owner.mjs +0 -0
  75. /package/runtime/scripts/{checks → hfs}/architecture/source-names.mjs +0 -0
  76. /package/runtime/scripts/{checks → hfs}/architecture/sql-owner.mjs +0 -0
  77. /package/runtime/scripts/{checks → hfs}/architecture/sql-tokens.mjs +0 -0
  78. /package/runtime/scripts/{checks → hfs}/architecture/symbols.mjs +0 -0
  79. /package/runtime/scripts/{checks → hfs}/architecture/tiers.mjs +0 -0
  80. /package/runtime/scripts/{checks → hfs}/architecture/transport-owner.mjs +0 -0
  81. /package/runtime/scripts/{checks → hfs}/architecture/unit-spec-providers.mjs +0 -0
  82. /package/runtime/scripts/{lib → hfs}/repo-identity.mjs +0 -0
  83. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/fe-no-tests.mjs +0 -0
  84. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/read.mjs +0 -0
  85. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/spec-placement.mjs +0 -0
  86. /package/runtime/scripts/{checks → hfs}/typescript-programs.mjs +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - Changed: `public-component-signature` accepts a component with no parameter; `no-raw-structural-element` names `Region`, `NavLandmark`, `List` and `ListItem` as owners of section, nav, ul/ol and li; FE-I18N-1 and FE-TYPING-6 canon text (request locale through `next/root-params`, no `void props`); the FE_SWR_KEY_IDENTITY check reads `&&` and `||` gates in its bundled runtime.
6
+ ## Unreleased (C0 batch)
7
+
8
+ - Changed (contract change `sonar-coverage-exclusions`): `route-slot-fixed-name` holds `error.tsx` to `ErrorBoundary`, no longer `Error`. `Error` shadows the global Error constructor (Sonar typescript:S2137, a bug), and React calls this component an error boundary. A slot still named `Error` is now reported.
9
+
3
10
  ## 8.0.7 - 2026-10-01
4
11
 
5
12
  - Changed: the same bundled runtime layer move.
@@ -12,7 +12,7 @@ Page structure and text are grammar components, not raw `div`-family HTML; only
12
12
 
13
13
  Applies to app source that composes the grammar (slots `fe.feature`, `fe.components`, `fe.route`, `fe.hooks`) and to every workspace package except the `<family>-ui` package (`fe.package.ui`), whose components are the renderers. Spec files render raw HTML as a harness and are not judged; a file no slot owns is not judged.
14
14
 
15
- The refused tags are exactly those `@starci/grammar` has a component for (the twin test reads the package source): `h1-h6` (`Heading`), `p` and `span` (`Text`), `hr` (`Divider`), `label` (`Label`), `form` (`Form`), `fieldset` (`Fieldset`), `dl` `dt` `dd` (`DescriptionList`), `table` `thead` `tbody` `tr` `td` `th` (`DataTable`), `ul` `ol` `li` (`SurfaceListCard`, `StaticStateRow`), `header` (`SectionHeader`, `TopBar`), `footer` (`Footer`), `nav` (`Subnav`, `Breadcrumbs`, `NavigationFeatureNav`), `section` (`SurfaceCard`), `main` (`WorkspaceShell`), `figure` and `figcaption` (`MediaFrame`). `div`, `br`, `aside` and `article` are not refused: the grammar has no component for them.
15
+ The refused tags are exactly those `@starci/grammar` has a component for (the twin test reads the package source): `h1-h6` (`Heading`), `p` and `span` (`Text`), `hr` (`Divider`), `label` (`Label`), `form` (`Form`), `fieldset` (`Fieldset`), `dl` `dt` `dd` (`DescriptionList`), `table` `thead` `tbody` `tr` `td` `th` (`DataTable`), `ul` `ol` `li` (`List` and `ListItem` for a plain semantic list, `SurfaceListCard`, `StaticStateRow`), `header` (`SectionHeader`, `TopBar`), `footer` (`Footer`), `nav` (`NavLandmark` for a named row of links, `Subnav`, `Breadcrumbs`, `NavigationFeatureNav`), `section` (`Region` for a named non-card region, `SurfaceCard`), `main` (`WorkspaceShell`), `figure` and `figcaption` (`MediaFrame`). `div`, `br`, `aside` and `article` are not refused: the grammar has no component for them.
16
16
 
17
17
  **Invalid** (`src/features/pages/home/component.tsx`)
18
18
 
package/file-layout.mjs CHANGED
@@ -381,7 +381,9 @@ const ROUTE_SLOT_NAME = {
381
381
  layout: "Layout",
382
382
  template: "Template",
383
383
  loading: "Loading",
384
- error: "Error",
384
+ // Not `Error`: that name shadows the global Error constructor (Sonar typescript:S2137, a bug), and React calls this
385
+ // component an error boundary.
386
+ error: "ErrorBoundary",
385
387
  "global-error": "GlobalError",
386
388
  "not-found": "NotFound",
387
389
  }
@@ -21,7 +21,7 @@ import { hfsOf } from "./lib/hfs.mjs"
21
21
 
22
22
  /**
23
23
  * The intrinsic elements of page structure and text, each with the grammar components that render it.
24
- * Read against packages/grammar/src (`export const <Component>`); `grammar-boundary.test.mjs` fails when one is not exported.
24
+ * Read against packages/grammar/src (`export const <Component>`); `grammar-boundary.spec.mjs` fails when one is not exported.
25
25
  */
26
26
  export const GRAMMAR_OWNERS = Object.freeze({
27
27
  h1: "Heading", h2: "Heading", h3: "Heading", h4: "Heading", h5: "Heading", h6: "Heading",
@@ -32,11 +32,11 @@ export const GRAMMAR_OWNERS = Object.freeze({
32
32
  fieldset: "Fieldset",
33
33
  dl: "DescriptionList", dt: "DescriptionList", dd: "DescriptionList",
34
34
  table: "DataTable", thead: "DataTable", tbody: "DataTable", tr: "DataTable", td: "DataTable", th: "DataTable",
35
- ul: "SurfaceListCard or StaticStateRow", ol: "SurfaceListCard or StaticStateRow", li: "SurfaceListCard or StaticStateRow",
35
+ ul: "List, SurfaceListCard or StaticStateRow", ol: "List, SurfaceListCard or StaticStateRow", li: "ListItem, SurfaceListCard or StaticStateRow",
36
36
  header: "SectionHeader or TopBar",
37
37
  footer: "Footer",
38
- nav: "Subnav, Breadcrumbs or NavigationFeatureNav",
39
- section: "SurfaceCard",
38
+ nav: "NavLandmark, Subnav, Breadcrumbs or NavigationFeatureNav",
39
+ section: "Region or SurfaceCard",
40
40
  main: "WorkspaceShell",
41
41
  figure: "MediaFrame", figcaption: "MediaFrame",
42
42
  })
package/lib/hfs.mjs CHANGED
@@ -4,8 +4,8 @@
4
4
  * A consuming `eslint.config.mjs` is the managed one-liner `export default starciFeConfig({ hfs: loadHfs(import.meta.url) })`.
5
5
  * The config lives in the fe/ side folder of an app; `loadHfs` finds the app-root `hfs.json` one level up and gives every file
6
6
  * linted under that folder the view of the fe side (paths relative to fe/), with the slot manifest this package ships in
7
- * `runtime/` (a byte copy of the runtime's `knowledge/hfs/slots.yaml`, `scripts/lib/hfs-slots.mjs` and
8
- * `scripts/lib/hfs-view.mjs`, refreshed by `packages/hfs/scripts/sync-runtime.mjs`), and returns the view the factory puts
7
+ * `runtime/` (a byte copy of the runtime's `knowledge/hfs/slots.yaml`, `scripts/hfs/slots.mjs` and
8
+ * `scripts/hfs/view.mjs`, refreshed by `scripts/hfs/sync-runtime.mjs`), and returns the view the factory puts
9
9
  * in `settings.starci.hfs`.
10
10
  *
11
11
  * Every path-scoped rule asks `slotOf` / `tierOf` here instead of testing a path with a regular expression. A rule that
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import { dirname, join } from "node:path"
15
15
  import { fileURLToPath } from "node:url"
16
- import { declaredHfsView, openHfsView } from "../runtime/scripts/lib/hfs-view.mjs"
16
+ import { declaredHfsView, openHfsView } from "../runtime/scripts/hfs/view.mjs"
17
17
 
18
18
  const RUNTIME = join(dirname(fileURLToPath(import.meta.url)), "..", "runtime")
19
19
 
package/lib/next.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Next.js's own contract for route segment files, read from the runtime's one statement of it
3
- * (`scripts/lib/next-contract.mjs`, shipped in `runtime/` by `packages/hfs/scripts/sync-runtime.mjs`), so every law
3
+ * (`scripts/lib/next-contract.mjs`, shipped in `runtime/` by `scripts/hfs/sync-runtime.mjs`), so every law
4
4
  * holds the same list by construction.
5
5
  */
6
6
  export { NEXT_RESERVED_EXPORTS, NEXT_ROUTE_SEGMENT_STEMS } from "../runtime/scripts/lib/next-contract.mjs"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starci/eslint-canon-fe",
3
- "version": "8.0.7",
3
+ "version": "8.0.8",
4
4
  "description": "The StarCi front-end lint canon: every rule its laws publish, gathered as one flat-config plugin and attached by one factory.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -15,7 +15,7 @@
15
15
  "runtime/**",
16
16
  "docs/*.md",
17
17
  "*.ts",
18
- "!*.test.mjs",
18
+ "!*.spec.mjs",
19
19
  "CHANGELOG.md",
20
20
  "README.md"
21
21
  ],
@@ -31,7 +31,7 @@
31
31
  "access": "public"
32
32
  },
33
33
  "scripts": {
34
- "test": "node --test \"*.test.mjs\""
34
+ "test": "node --test \"*.spec.mjs\""
35
35
  },
36
36
  "keywords": [
37
37
  "eslint",
package/project-graph.mjs CHANGED
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * The rules that read the project graph: the findings of the architecture machine that sit on a TypeScript file.
3
3
  *
4
- * One graph per lint run: `scripts/lib/project-graph.mjs` (a byte copy in this package's runtime/) builds the machine's module
4
+ * One graph per lint run: `scripts/hfs/project-graph.mjs` (a byte copy in this package's runtime/) builds the machine's module
5
5
  * graph and slot manifest once for the repository, caches it, and each rule below reports the findings of its own codes on
6
6
  * the file ESLint is visiting, on the line the machine names. There is no second graph and no second judge: the list of
7
- * rules and their codes is `LINT_ENFORCERS` of the machine (scripts/checks/architecture/surface.mjs); `hfs check` keeps the
7
+ * rules and their codes is `LINT_ENFORCERS` of the machine (scripts/hfs/architecture/surface.mjs); `hfs check` keeps the
8
8
  * findings that have no TypeScript file to sit on. A graph that cannot be built stops the run with the machine's message.
9
9
  */
10
- import { LINT_ENFORCERS } from "./runtime/scripts/checks/architecture/surface.mjs"
11
- import { projectRule } from "./runtime/scripts/lib/project-rule.mjs"
10
+ import { LINT_ENFORCERS } from "./runtime/scripts/hfs/architecture/surface.mjs"
11
+ import { projectRule } from "./runtime/scripts/hfs/project-rule.mjs"
12
12
  import { hfsOf } from "./lib/hfs.mjs"
13
13
  import { dirname, join } from "node:path"
14
14
  import { fileURLToPath } from "node:url"
@@ -44,14 +44,14 @@ const componentNameOf = (node) => {
44
44
  return node.id.name
45
45
  }
46
46
 
47
- /** Require exported React components to expose one named props parameter. */
47
+ /** Require exported React components to take no parameter, or one named props parameter. */
48
48
  export const publicComponentSignature = {
49
49
  meta: {
50
50
  type: "problem",
51
- docs: { description: "Exported React components use one parameter named props with a matching XProps type (XBase may also take XBaseProps)." },
51
+ docs: { description: "Exported React components take no parameter, or one parameter named props with a matching XProps type (XBase may also take XBaseProps)." },
52
52
  schema: [],
53
53
  messages: {
54
- parameter: "Exported component {{name}} must be an arrow const with exactly one parameter named props.",
54
+ parameter: "Exported component {{name}} must be an arrow const with no parameter, or exactly one parameter named props.",
55
55
  type: "Exported component {{name}} must type props as {{expected}}.",
56
56
  },
57
57
  },
@@ -59,6 +59,7 @@ export const publicComponentSignature = {
59
59
  const check = (node, name, fn) => {
60
60
  if (!name || !fn) return
61
61
  const params = fn.params || []
62
+ if (params.length === 0) return
62
63
  if (params.length !== 1 || params[0].type !== "Identifier" || params[0].name !== "props") {
63
64
  context.report({ node: fn, messageId: "parameter", data: { name } })
64
65
  return
@@ -16,6 +16,11 @@ const moduleRoot = path.dirname(fileURLToPath(new URL('../package.json', import.
16
16
 
17
17
  /** The runtime root: this source tree (or an immutable sealed payload of it). */
18
18
  export const skillRoot = moduleRoot;
19
+ /**
20
+ * The StarCi Source root (the host containing .claude and .workspaces): STARCI_SOURCE_ROOT overrides it, else the directory
21
+ * holding this runtime checkout. Parameterized over `env`, so a spec can inject a fixture Source.
22
+ */
23
+ export const starciSourceRoot = (env = process.env) => (env.STARCI_SOURCE_ROOT ? path.resolve(env.STARCI_SOURCE_ROOT) : path.dirname(skillRoot));
19
24
 
20
25
  /**
21
26
  * Read a runtime contract document. `parts` are path segments under `skillRoot`
@@ -1,8 +1,8 @@
1
1
  // engine/yaml.mjs — GENERATED, frozen. This is the yaml@2.9.0 package bundled by esbuild, with the
2
2
  // runtime's parseYaml/stringifyYaml wrapper as the entry module (source shown below). The installed
3
- // runtime carries zero npm dependencies (tests/npm-package.spec.mjs), so it vendors this parser
3
+ // runtime carries zero npm dependencies (tests/repo/npm-package.spec.mjs), so it vendors this parser
4
4
  // rather than importing node_modules; yaml is a devDependency and a direct import breaks that
5
- // install. tests/yaml-vendored-parity.spec.mjs proves the vendored parseYaml/stringifyYaml behave
5
+ // install. tests/engine/yaml-vendored-parity.spec.mjs proves the vendored parseYaml/stringifyYaml behave
6
6
  // exactly like yaml@2.9.0 with these options for every tracked *.yaml/*.yml file (parsed values
7
7
  // deep-equal, stringify output byte-identical). License: engine/yaml-license.json.
8
8
  // Rebuild only when deliberately upgrading yaml. The entry source is:
@@ -133,7 +133,7 @@ ${t.comment}`:t.comment}this.doc.range[2]=t.offset;break}default:this.errors.pus
133
133
  `),i=e.substring(0,t),n=e.substring(t+1)+`
134
134
  `;if(s.type==="block-scalar"){let r=s.props[0];if(r.type!=="block-scalar-header")throw new Error("Invalid block scalar header");r.source=i,s.source=n}else{let{offset:r}=s,a="indent"in s?s.indent:-1,l=[{type:"block-scalar-header",offset:r,indent:a,source:i}];Xr(l,"end"in s?s.end:void 0)||l.push({type:"newline",offset:-1,indent:a,source:`
135
135
  `});for(let c of Object.keys(s))c!=="type"&&c!=="offset"&&delete s[c];Object.assign(s,{type:"block-scalar",indent:a,props:l,source:n})}}o(Af,"setBlockScalarValue");function Xr(s,e){if(e)for(let t of e)switch(t.type){case"space":case"comment":s.push(t);break;case"newline":return s.push(t),!0}return!1}o(Xr,"addEndtoBlockProps");function pi(s,e,t){switch(s.type){case"scalar":case"double-quoted-scalar":case"single-quoted-scalar":s.type=t,s.source=e;break;case"block-scalar":{let i=s.props.slice(1),n=e.length;s.props[0].type==="block-scalar-header"&&(n-=s.props[0].source.length);for(let r of i)r.offset+=n;delete s.props,Object.assign(s,{type:t,source:e,end:i});break}case"block-map":case"block-seq":{let n={type:"newline",offset:s.offset+e.length,indent:s.indent,source:`
136
- `};delete s.items,Object.assign(s,{type:t,source:e,end:[n]});break}default:{let i="indent"in s?s.indent:-1,n="end"in s&&Array.isArray(s.end)?s.end.filter(r=>r.type==="space"||r.type==="comment"||r.type==="newline"):[];for(let r of Object.keys(s))r!=="type"&&r!=="offset"&&delete s[r];Object.assign(s,{type:t,indent:i,source:e,end:n})}}}o(pi,"setFlowScalarValue");jt.createScalarToken=kf;jt.resolveAsScalar=vf;jt.setScalarValue=Nf});var ea=y(Zr=>{"use strict";var qf=o(s=>"type"in s?Rt(s):Ft(s),"stringify");function Rt(s){switch(s.type){case"block-scalar":{let e="";for(let t of s.props)e+=Rt(t);return e+s.source}case"block-map":case"block-seq":{let e="";for(let t of s.items)e+=Ft(t);return e}case"flow-collection":{let e=s.start.source;for(let t of s.items)e+=Ft(t);for(let t of s.end)e+=t.source;return e}case"document":{let e=Ft(s);if(s.end)for(let t of s.end)e+=t.source;return e}default:{let e=s.source;if("end"in s&&s.end)for(let t of s.end)e+=t.source;return e}}}o(Rt,"stringifyToken");function Ft({start:s,key:e,sep:t,value:i}){let n="";for(let r of s)n+=r.source;if(e&&(n+=Rt(e)),t)for(let r of t)n+=r.source;return i&&(n+=Rt(i)),n}o(Ft,"stringifyItem");Zr.stringify=qf});var na=y(ia=>{"use strict";var mi=Symbol("break visit"),Of=Symbol("skip children"),ta=Symbol("remove item");function re(s,e){"type"in s&&s.type==="document"&&(s={start:s.start,value:s.value}),sa(Object.freeze([]),s,e)}o(re,"visit");re.BREAK=mi;re.SKIP=Of;re.REMOVE=ta;re.itemAtPath=(s,e)=>{let t=s;for(let[i,n]of e){let r=t?.[i];if(r&&"items"in r)t=r.items[n];else return}return t};re.parentCollection=(s,e)=>{let t=re.itemAtPath(s,e.slice(0,-1)),i=e[e.length-1][0],n=t?.[i];if(n&&"items"in n)return n;throw new Error("Parent collection not found")};function sa(s,e,t){let i=t(e,s);if(typeof i=="symbol")return i;for(let n of["key","value"]){let r=e[n];if(r&&"items"in r){for(let a=0;a<r.items.length;++a){let l=sa(Object.freeze(s.concat([[n,a]])),r.items[a],t);if(typeof l=="number")a=l-1;else{if(l===mi)return mi;l===ta&&(r.items.splice(a,1),a-=1)}}typeof i=="function"&&n==="key"&&(i=i(e,s))}}return typeof i=="function"?i(e,s):i}o(sa,"_visit");ia.visit=re});var Yt=y($=>{"use strict";var gi=zr(),Lf=ea(),Ef=na(),yi="\uFEFF",bi="",Si="",wi="",Tf=o(s=>!!s&&"items"in s,"isCollection"),Cf=o(s=>!!s&&(s.type==="scalar"||s.type==="single-quoted-scalar"||s.type==="double-quoted-scalar"||s.type==="block-scalar"),"isScalar");function If(s){switch(s){case yi:return"<BOM>";case bi:return"<DOC>";case Si:return"<FLOW_END>";case wi:return"<SCALAR>";default:return JSON.stringify(s)}}o(If,"prettyToken");function Mf(s){switch(s){case yi:return"byte-order-mark";case bi:return"doc-mode";case Si:return"flow-error-end";case wi:return"scalar";case"---":return"doc-start";case"...":return"doc-end";case"":case`
136
+ `};delete s.items,Object.assign(s,{type:t,source:e,end:[n]});break}default:{let i="indent"in s?s.indent:-1,n="end"in s&&Array.isArray(s.end)?s.end.filter(r=>r.type==="space"||r.type==="comment"||r.type==="newline"):[];for(let r of Object.keys(s))r!=="type"&&r!=="offset"&&delete s[r];Object.assign(s,{type:t,indent:i,source:e,end:n})}}}o(pi,"setFlowScalarValue");jt.createScalarToken=kf;jt.resolveAsScalar=vf;jt.setScalarValue=Nf});var ea=y(Zr=>{"use strict";var qf=o(s=>"type"in s?Rt(s):Ft(s),"stringify");function Rt(s){switch(s.type){case"block-scalar":{let e="";for(let t of s.props)e+=Rt(t);return e+s.source}case"block-map":case"block-seq":{let e="";for(let t of s.items)e+=Ft(t);return e}case"flow-collection":{let e=s.start.source;for(let t of s.items)e+=Ft(t);for(let t of s.end)e+=t.source;return e}case"document":{let e=Ft(s);if(s.end)for(let t of s.end)e+=t.source;return e}default:{let e=s.source;if("end"in s&&s.end)for(let t of s.end)e+=t.source;return e}}}o(Rt,"stringifyToken");function Ft({start:s,key:e,sep:t,value:i}){let n="";for(let r of s)n+=r.source;if(e&&(n+=Rt(e)),t)for(let r of t)n+=r.source;return i&&(n+=Rt(i)),n}o(Ft,"stringifyItem");Zr.stringify=qf});var na=y(ia=>{"use strict";var mi=Symbol("break visit"),Of=Symbol("skip children"),ta=Symbol("remove item");function re(s,e){"type"in s&&s.type==="document"&&(s={start:s.start,value:s.value}),sa(Object.freeze([]),s,e)}o(re,"visit");re.BREAK=mi;re.SKIP=Of;re.REMOVE=ta;re.itemAtPath=(s,e)=>{let t=s;for(let[i,n]of e){let r=t?.[i];if(r&&"items"in r)t=r.items[n];else return}return t};re.parentCollection=(s,e)=>{let t=re.itemAtPath(s,e.slice(0,-1)),i=e[e.length-1][0],n=t?.[i];if(n&&"items"in n)return n;throw new Error("Parent collection not found")};function sa(s,e,t){let i=t(e,s);if(typeof i=="symbol")return i;for(let n of["key","value"]){let r=e[n];if(r&&"items"in r){for(let a=0;a<r.items.length;++a){let l=sa(Object.freeze(s.concat([[n,a]])),r.items[a],t);if(typeof l=="number")a=l-1;else{if(l===mi)return mi;l===ta&&(r.items.splice(a,1),a-=1)}}typeof i=="function"&&n==="key"&&(i=i(e,s))}}return typeof i=="function"?i(e,s):i}o(sa,"_visit");ia.visit=re});var Yt=y($=>{"use strict";var gi=zr(),Lf=ea(),Ef=na(),yi="\uFEFF",bi="\u0002",Si="\u0018",wi="\u001f",Tf=o(s=>!!s&&"items"in s,"isCollection"),Cf=o(s=>!!s&&(s.type==="scalar"||s.type==="single-quoted-scalar"||s.type==="double-quoted-scalar"||s.type==="block-scalar"),"isScalar");function If(s){switch(s){case yi:return"<BOM>";case bi:return"<DOC>";case Si:return"<FLOW_END>";case wi:return"<SCALAR>";default:return JSON.stringify(s)}}o(If,"prettyToken");function Mf(s){switch(s){case yi:return"byte-order-mark";case bi:return"doc-mode";case Si:return"flow-error-end";case wi:return"scalar";case"---":return"doc-start";case"...":return"doc-end";case"":case`
137
137
  `:case`\r
138
138
  `:return"newline";case"-":return"seq-item-ind";case"?":return"explicit-key-ind";case":":return"map-value-ind";case"{":return"flow-map-start";case"}":return"flow-map-end";case"[":return"flow-seq-start";case"]":return"flow-seq-end";case",":return"comma"}switch(s[0]){case" ":case" ":return"space";case"#":return"comment";case"%":return"directive-line";case"*":return"alias";case"&":return"anchor";case"!":return"tag";case"'":return"single-quoted-scalar";case'"':return"double-quoted-scalar";case"|":case">":return"block-scalar-header"}return null}o(Mf,"tokenType");$.createScalarToken=gi.createScalarToken;$.resolveAsScalar=gi.resolveAsScalar;$.setScalarValue=gi.setScalarValue;$.stringify=Lf.stringify;$.visit=Ef.visit;$.BOM=yi;$.DOCUMENT=bi;$.FLOW_END=Si;$.SCALAR=wi;$.isCollection=Tf;$.isScalar=Cf;$.prettyToken=If;$.tokenType=Mf});var Ni=y(aa=>{"use strict";var Je=Yt();function K(s){switch(s){case void 0:case" ":case`
139
139
  `:case"\r":case" ":return!0;default:return!1}}o(K,"isEmpty");var ra=new Set("0123456789ABCDEFabcdef"),Pf=new Set("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-#;/?:@&=+$_.!~*'()"),Ut=new Set(",[]{}"),_f=new Set(` ,[]{}
@@ -17,68 +17,68 @@ pins:
17
17
  # --- @starci packages. Every @starci package is a PUBLISHED npm package (install: registry): a product repository
18
18
  # installs the exact pinned version from the npm registry in the root and every workspace package.json, never `file:`.
19
19
  '@starci/heroicons':
20
- version: 0.3.1
20
+ version: 0.3.2
21
21
  group: starci
22
22
  install: registry
23
23
  side: fe
24
24
  source: packages/heroicons/package.json
25
25
  why: 'StarCi custom Heroicons-compatible cuts; eslint-canon-fe (icon.mjs) admits @starci/heroicons/24/outline and /16/solid as icon sources, so an app that uses them gets this exact version.'
26
26
  '@starci/grammar':
27
- version: 0.8.1
27
+ version: 0.8.2
28
28
  group: starci
29
29
  install: registry
30
30
  side: fe
31
31
  source: packages/grammar/package.json
32
32
  why: nivo-fe pins 0.4.11 and 0.5.0 in one workspace, starci-next-fe 0.5.1, miamia-fe 0.5.0; the runtime source is 0.8.0 (the brand layer sets `--font-sans` and `--font-mono`; the grammar reads them; 0.7.2 added the Input tel kind and IconButton disclosure props).
33
33
  '@starci/eslint-canon-be':
34
- version: 3.0.7
34
+ version: 3.0.8
35
35
  group: starci
36
36
  install: registry
37
37
  side: be
38
38
  source: packages/eslint/be/package.json
39
39
  why: '3.0.3: its bundled canon-pins copy pins stylelint-canon 2.0.2 and hfs 4.0.3; no rule changed. 3.0.2: its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root (app.starcistacks, app.sops) and pin hfs 4.0.2. 3.0.1: its bundled canon-pins copy pins hfs 4.0.1. 3.0.0: `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; the project graph is built per side. 2.0.0 (C0 release): starciBeConfig({ hfs: loadHfs(import.meta.url) }) typed factory and the BE-CONVENTION laws.'
40
40
  '@starci/eslint-canon-fe':
41
- version: 8.0.7
41
+ version: 8.0.8
42
42
  group: starci
43
43
  install: registry
44
44
  side: fe
45
45
  source: packages/eslint/fe/package.json
46
46
  why: '8.0.3: its bundled canon-pins copy pins stylelint-canon 2.0.2 and hfs 4.0.3; no rule changed. 8.0.2: its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root (app.starcistacks, app.sops) and pin hfs 4.0.2. 8.0.1: its bundled canon-pins copy pins hfs 4.0.1. 8.0.0: `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; the project graph is built per side. 7.0.0: the front end has no tests (FE_NO_TESTS R97 in hfs); no-vietnamese-in-source (R91).'
47
47
  '@starci/stylelint-canon':
48
- version: 2.0.2
48
+ version: 2.0.3
49
49
  group: starci
50
50
  install: registry
51
51
  side: fe
52
52
  source: packages/stylelint/package.json
53
53
  why: '2.0.2: no-class-selector refuses a compound class selector (`div.card`), which the 2.0.1 pattern let through. 2.0.1 accepts `--font-sans` and `--font-mono` in the brand layer (the vocabulary follows the grammar 0.8.0). 2.0.0 adds status-contrast (HeroUI soft pairs measured per theme), fixes brand-layer-shape on the shared :root,.light,.dark block and the info soft pair, and derives appTokens with loadAppTokens(import.meta.url) for the managed one-line stylelint.config.mjs.'
54
54
  '@starci/tsconfig':
55
- version: 2.0.1
55
+ version: 2.0.2
56
56
  group: starci
57
57
  install: registry
58
58
  side: both
59
59
  source: packages/tsconfig/package.json
60
60
  '@starci/prettier-config':
61
- version: 1.0.0
61
+ version: 1.0.1
62
62
  group: starci
63
63
  install: registry
64
64
  side: both
65
65
  source: packages/prettier-config/package.json
66
66
  '@starci/jest-preset':
67
- version: 2.2.2
67
+ version: 2.2.3
68
68
  group: starci
69
69
  install: registry
70
70
  side: be
71
71
  source: packages/jest-preset/package.json
72
72
  why: '2.2.2: the unit run writes the lcov Sonar and Codecov import (services only); 2.2.0: the integration, e2e and contract projects run every spec file in a worker process of its own (world-runner.cjs), so a process-global framework registry (@nestjs/graphql type metadata) never leaks between e2e files; 2.1.0: four projects on the test world, the unit kit (mockEntityManager, fakeTransaction, fakeIds, FakeClock, Outcome matchers) and the recordingOutbox claim side.'
73
73
  '@starci/test-world':
74
- version: 1.0.5
74
+ version: 1.0.6
75
75
  group: starci
76
76
  install: registry
77
77
  side: be
78
78
  source: packages/test-world/package.json
79
79
  why: '1.0.5: a modules world boots real peer apps beside its modules, world.apps.<name>.during(fn) is the outage of a peer app, and w.keycloak.clientSecret(client) answers the run-generated secret of a confidential realm client. 1.0.4: world.resolve and scope.resolve take any Nest token (class, string or symbol). 1.0.3: the globalSetup registers the path aliases as TypeScript resolves them (the extends chain, paths from the config that declares them, the effective baseUrl), so a tests tsconfig that only extends the side config loads the declaration. 1.0.2: every path a declaration names (`stack`, seeds, the realm, Dockerfiles) resolves from the app root (the directory of hfs.json), where .starcistacks lives, never from the be side. The shared e2e library of every back end (R47, R48): the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs, the outage lock and the starci-test-stack bin behind the managed test:stack script; a devDependency of every back end. 1.0.1: world.keycloak.events/sessions (the user events of the realm and live sessions through the admin API) and world.infra.postgresql.connection(name) (the database of one connection down while the others serve, under the outage lock).'
80
80
  '@starci/hfs':
81
- version: 4.0.7
81
+ version: 4.0.8
82
82
  group: starci
83
83
  install: registry
84
84
  side: both
@@ -4,7 +4,7 @@
4
4
  # the CI, the hooks and .starciwork (slots of profile `app`); `be/` and `fe/` are its two sides, each laid out as the old
5
5
  # standalone repository root (slots of profile be or fe, paths relative to the side folder). Every tracked path of every
6
6
  # repository must match exactly one slot. Checks, lint factories, the architecture machine, templates and the why
7
- # catalog READ this file through scripts/lib/hfs-slots.mjs; none of them hardcodes a path.
7
+ # catalog READ this file through scripts/hfs/slots.mjs; none of them hardcodes a path.
8
8
  # Shape: modules/schemas/hfs-slots.schema.yaml. The repository side is hfs.json: modules/schemas/hfs-repo.schema.yaml.
9
9
  schema: starci/hfs-slots@2
10
10
  version: 2.0.0
@@ -97,7 +97,7 @@ tiers:
97
97
  crossOwner: every import across owners targets the owner's public entry (index.ts or index.tsx); never export *.
98
98
  crossApp: apps never import each other; shared code is a packages/<pkg> slot.
99
99
 
100
- # Parameters the lint factories and checks read per profile (through ruleParams(profile) in scripts/lib/hfs-slots.mjs).
100
+ # Parameters the lint factories and checks read per profile (through ruleParams(profile) in scripts/hfs/slots.mjs).
101
101
  ruleParams:
102
102
  be:
103
103
  # HFS_SIZE_GROWTH: a file above soft may not grow against its parent commit; a new file stays within soft.
@@ -1166,9 +1166,9 @@ slots:
1166
1166
 
1167
1167
  # Checks that read this manifest (all ship from .claude; none keeps its own path list)
1168
1168
  consumers:
1169
- - scripts/lib/hfs-slots.mjs # the loader every consumer below goes through
1170
- - scripts/lib/hfs-check.mjs # hfs check: HFS_SLOT_* / tracked / external, the side view per side
1171
- - scripts/checks/architecture.mjs # tiers, direction matrix, cycles, reachability
1169
+ - scripts/hfs/slots.mjs # the loader every consumer below goes through
1170
+ - scripts/hfs/check.mjs # hfs check: HFS_SLOT_* / tracked / external, the side view per side
1171
+ - scripts/hfs/architecture.mjs # tiers, direction matrix, cycles, reachability
1172
1172
  - packages/eslint/be starciBeConfig({hfs}) # file globs for rules come from slots
1173
1173
  - packages/eslint/fe starciFeConfig({hfs})
1174
1174
  - packages/stylelint # @starci/stylelint-canon: colour and brand allowances from fe.modules.brand
@@ -40,11 +40,11 @@ rules:
40
40
  fe.source-root-pinned, even though the machine list names it as a file Next loads. Any other file at the source
41
41
  root, including a helper folder beside a pinned file, has no owner and is refused.
42
42
  frameworkPinnedRootFiles:
43
- # Machine list read by scripts/checks/architecture/framework-pinned.mjs (FE_SOURCE_LAYOUT_INVALID accepts exactly
43
+ # Machine list read by scripts/hfs/architecture/framework-pinned.mjs (FE_SOURCE_LAYOUT_INVALID accepts exactly
44
44
  # these basenames directly in a Next source root; FE_TIER_DIRECTION keeps them thin). Exact names,
45
45
  # case-sensitive, no globs. It lists what the framework loads from the source root. Which of them a repository may
46
46
  # keep is decided by the slot fe.source-root-pinned and FE_NEXT_CONVENTIONS, not by this list.
47
- # Machine list read by scripts/checks/architecture/framework-pinned.mjs (FE_SOURCE_LAYOUT_INVALID
47
+ # Machine list read by scripts/hfs/architecture/framework-pinned.mjs (FE_SOURCE_LAYOUT_INVALID
48
48
  # accepts exactly these basenames directly in a Next source root;
49
49
  # their imports follow the FE_TIER_DIRECTION matrix). Exact names, case-sensitive, no globs. Supervisor rulings for nivo
50
50
  # wf-nivo-fe-debt-mug06w7h inc-2e42a24b74e4 and inc-846867b9a34e (proxy is the Next 16 name of
@@ -4,7 +4,7 @@ title: The Sonar quality gate every product is held to
4
4
  purpose: |
5
5
  The one place the Sonar thresholds live. A product repository never restates them: its
6
6
  .starcistacks/application-stacks.yaml `services.sonar.qualityGate` names the gate below, and
7
- scripts/checks/sonar-local.mjs (1) makes the local SonarQube's gate of that name carry exactly these
7
+ scripts/gates/sonar-local.mjs (1) makes the local SonarQube's gate of that name carry exactly these
8
8
  conditions and selects it for the project, and (2) judges an op's slice - the lines it changed - against
9
9
  the same numbers. Code-writing ops cannot settle done while the slice is red
10
10
  (scripts/kernel/sonar-settle.mjs). Changing a number here changes every repository on the next scan.
@@ -20,7 +20,7 @@ gate:
20
20
  newCode:
21
21
  # Coverage of the services only. The be unit run writes coverage/lcov.info (@starci/jest-preset coverageReporters lcov) and the
22
22
  # managed sonar-project.properties imports it (sonar.javascript.lcov.reportPaths=be/coverage/lcov.info) with
23
- # sonar.coverage.inclusions=be/src/**/*.service.ts, the one scope rendered by hfs sync from the preset's COVERAGE_SOURCES (codecov.yml
23
+ # sonar.coverage.exclusions = the complement of be/src/**/*.service.ts (SonarQube has no coverage inclusions), rendered by hfs sync from the preset's COVERAGE_SOURCES and the slot manifest (codecov.yml
24
24
  # is rendered from the same constant). A handler, resolver, controller, module, config file or test is not a coverage target and
25
25
  # fe/ is outside coverage entirely, so coverage on these conditions is the services' coverage alone.
26
26
  coverage:
@@ -1,4 +1,4 @@
1
- // scripts/api/git/lib.mjs — the one place the runtime spawns git (scripts/checks/check-layers.mjs enforces it).
1
+ // scripts/api/git/lib.mjs — the one place the runtime spawns git (RT_EXTERNAL_OWNER of scripts/hfs/runtime-rules/external-owner.mjs enforces it).
2
2
  //
3
3
  // Every caller spelt the same options by hand - encoding:'utf8', windowsHide:true, sometimes a timeout - with two shapes:
4
4
  // `git args` in a cwd and `git -C dir args`. gitOutput is the throwing shape (stdout text, or an Error on a non-zero exit).
@@ -0,0 +1,19 @@
1
+ // recorded-lines.mjs - the line count a file had at a git revision (git show <ref>:<file>): the recorded size a
2
+ // size-growth rule compares the working file with.
3
+ import path from 'node:path';
4
+ import { runGit } from './lib.mjs';
5
+ import { lineCount } from '../../lib/line-count.mjs';
6
+
7
+ /**
8
+ * The line count of a file at a git revision, or null when the file did not exist there or git is absent.
9
+ *
10
+ * @param {string} filename - Absolute path of the file.
11
+ * @param {string} ref - The revision, `HEAD` by default (the parent of the commit being made).
12
+ * @returns {number | null} The recorded line count.
13
+ */
14
+ export const recordedLines = (filename, ref = 'HEAD') => {
15
+ const r = runGit(['show', `${ref}:./${path.basename(filename)}`], { dir: path.dirname(filename), maxBuffer: 64 * 1024 * 1024 });
16
+ // no repository, no such revision, or a new file: all mean there is no recorded size to compare with
17
+ if (r.error || r.status !== 0) return null;
18
+ return lineCount(r.stdout);
19
+ };
@@ -1,4 +1,4 @@
1
- // decrypt.mjs — one `sops` decryption: the custody reads (scripts/lib/sops-exec-env.mjs readCustody) and the test
1
+ // decrypt.mjs — one `sops` decryption: the custody reads (scripts/api/sops/exec-env.mjs execEnv) and the test
2
2
  // secrets (scripts/lib/test-secrets.mjs testSecret). The plaintext stays in this process's memory (stdout over a pipe).
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
@@ -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
+ }