@starci/hfs 5.3.0 → 5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +5 -3
  3. package/cache/test-family.d.mts +1 -1
  4. package/cache/test-family.mjs +20 -15
  5. package/codemod/account-doors.mjs +7 -1
  6. package/codemod/app-prettier.d.mts +5 -0
  7. package/codemod/app-prettier.mjs +27 -0
  8. package/codemod/explicit-surface.mjs +1 -25
  9. package/codemod/main.mjs +14 -5
  10. package/codemod/module-definition.d.mts +10 -3
  11. package/codemod/module-definition.mjs +32 -17
  12. package/codemod/seeds-layout.d.mts +36 -0
  13. package/codemod/seeds-layout.mjs +113 -0
  14. package/codemod/sops-rules.d.mts +1 -1
  15. package/codemod/sops-rules.mjs +8 -3
  16. package/lint/run.d.mts +5 -4
  17. package/lint/run.mjs +53 -27
  18. package/package.json +1 -1
  19. package/release/lint-report.mjs +4 -3
  20. package/runtime/knowledge/hfs/canon-pins.yaml +12 -12
  21. package/runtime/knowledge/hfs/ci-standard.yaml +2 -2
  22. package/runtime/knowledge/hfs/rules.yaml +15 -2
  23. package/runtime/knowledge/hfs/slots.yaml +15 -18
  24. package/runtime/modules/kernel/failure-codes.yaml +64 -3
  25. package/runtime/scripts/hfs/architecture/root-packages.mjs +1 -1
  26. package/runtime/scripts/hfs/check.mjs +4 -1
  27. package/runtime/scripts/hfs/coverage-scope.d.mts +15 -0
  28. package/runtime/scripts/hfs/coverage-scope.mjs +26 -0
  29. package/runtime/scripts/hfs/db-squash.mjs +9 -2
  30. package/runtime/scripts/hfs/declaration-shape.mjs +3 -1
  31. package/runtime/scripts/hfs/rules/app-root.mjs +4 -1
  32. package/runtime/scripts/hfs/rules/ci-standard.mjs +5 -4
  33. package/runtime/scripts/hfs/rules/coverage-below.mjs +12 -4
  34. package/runtime/scripts/hfs/rules/database-config.mjs +8 -7
  35. package/runtime/scripts/hfs/rules/database-constants.d.mts +2 -1
  36. package/runtime/scripts/hfs/rules/database-constants.mjs +2 -1
  37. package/runtime/scripts/hfs/rules/database.d.mts +2 -2
  38. package/runtime/scripts/hfs/rules/database.mjs +7 -6
  39. package/runtime/scripts/hfs/rules/docker.mjs +24 -7
  40. package/runtime/scripts/hfs/rules/monorepo.d.mts +1 -1
  41. package/runtime/scripts/hfs/rules/monorepo.mjs +1 -1
  42. package/runtime/scripts/hfs/rules/seeds.d.mts +21 -0
  43. package/runtime/scripts/hfs/rules/seeds.mjs +194 -0
  44. package/runtime/scripts/hfs/rules/stacks.mjs +8 -4
  45. package/runtime/scripts/hfs/slots.d.mts +2 -0
  46. package/runtime/scripts/hfs/slots.mjs +1 -0
  47. package/runtime/scripts/hfs/stack-seeds-config.d.mts +88 -0
  48. package/runtime/scripts/hfs/stack-seeds-config.mjs +239 -0
  49. package/runtime/scripts/hfs/view.d.mts +25 -0
  50. package/runtime/scripts/hfs/view.mjs +41 -0
  51. package/scaffold/add-app.mjs +2 -2
  52. package/scaffold/add-cli-lite.mjs +41 -14
  53. package/scaffold/add-module.mjs +6 -3
  54. package/scaffold/lite-exports.mjs +1 -1
  55. package/scaffold/module-registry.mjs +10 -8
  56. package/src/main.mjs +8 -8
  57. package/stack/deploy.d.mts +3 -0
  58. package/stack/deploy.mjs +4 -1
  59. package/stack/local-up.mjs +25 -3
  60. package/stack/seeds.d.mts +29 -0
  61. package/stack/seeds.mjs +197 -0
  62. package/sync/canon-layout.d.mts +38 -0
  63. package/sync/canon-layout.mjs +62 -0
  64. package/sync/ci-commands.d.mts +2 -1
  65. package/sync/ci-commands.mjs +10 -7
  66. package/sync/compile-guard.d.mts +36 -0
  67. package/sync/compile-guard.mjs +189 -0
  68. package/sync/index.d.mts +22 -3
  69. package/sync/index.mjs +80 -32
  70. package/templates/app/ci-workflows/github/workflows/ci.yml +2 -2
  71. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +2 -2
  72. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +26 -0
  73. package/templates/app/skeleton/.starcistacks/dev/README.md +21 -10
  74. package/templates/app/skeleton/.starcistacks/dev/seeds/data/.gitkeep +0 -0
  75. package/templates/app/skeleton/.starcistacks/dev/seeds/files/.gitkeep +0 -0
  76. package/templates/app/skeleton/.starcistacks/dev/seeds/seeds.yaml +3 -0
  77. package/templates/app/skeleton/.starcistacks/production/seeds/data/.gitkeep +0 -0
  78. package/templates/app/skeleton/.starcistacks/production/seeds/files/.gitkeep +0 -0
  79. package/templates/app/skeleton/.starcistacks/production/seeds/seeds.yaml +2 -0
  80. package/templates/app/skeleton/.starcistacks/production/seeds/sql/.gitkeep +0 -0
  81. package/templates/app/skeleton/.starcistacks/seeds/data/.gitkeep +0 -0
  82. package/templates/app/skeleton/.starcistacks/seeds/files/.gitkeep +0 -0
  83. package/templates/app/skeleton/.starcistacks/seeds/seeds.yaml +2 -0
  84. package/templates/app/skeleton/.starcistacks/seeds/sql/.gitkeep +0 -0
  85. package/templates/app/skeleton/README.md +1 -1
  86. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +2 -2
  87. package/templates/app/skeleton-lite/.starcistacks/dev/seeds/data/.gitkeep +0 -0
  88. package/templates/app/skeleton-lite/.starcistacks/dev/seeds/files/.gitkeep +0 -0
  89. package/templates/app/skeleton-lite/.starcistacks/dev/seeds/seeds.yaml +2 -0
  90. package/templates/app/skeleton-lite/.starcistacks/dev/seeds/sql/.gitkeep +0 -0
  91. package/templates/app/skeleton-lite/.starcistacks/production/seeds/data/.gitkeep +0 -0
  92. package/templates/app/skeleton-lite/.starcistacks/production/seeds/files/.gitkeep +0 -0
  93. package/templates/app/skeleton-lite/.starcistacks/production/seeds/seeds.yaml +2 -0
  94. package/templates/app/skeleton-lite/.starcistacks/production/seeds/sql/.gitkeep +0 -0
  95. package/templates/app/skeleton-lite/.starcistacks/seeds/data/.gitkeep +0 -0
  96. package/templates/app/skeleton-lite/.starcistacks/seeds/files/.gitkeep +0 -0
  97. package/templates/app/skeleton-lite/.starcistacks/seeds/seeds.yaml +2 -0
  98. package/templates/app/skeleton-lite/.starcistacks/seeds/sql/.gitkeep +0 -0
  99. package/templates/app/skeleton-lite/README.md +2 -2
  100. package/templates/app/skeleton-lite/db/migrations/__timestamp___baseline.sql +3 -1
  101. package/templates/be/auth-lite/src/features/api/auth/application/auth.contracts.ts +2 -0
  102. package/templates/be/auth-lite/src/features/api/auth/application/signup.handler.ts +6 -1
  103. package/templates/be/auth-lite/src/features/api/auth/auth.module.ts +14 -1
  104. package/templates/be/auth-lite/src/features/api/auth/transport/http/auth.controller.ts +5 -1
  105. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/reset.request.ts +0 -1
  106. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/signup.request.ts +7 -0
  107. package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.spec.ts +15 -0
  108. package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.ts +9 -3
  109. package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.spec.ts +79 -16
  110. package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.ts +39 -9
  111. package/templates/be/auth-lite/src/modules/domain/identity/identity-email.service.spec.ts +108 -20
  112. package/templates/be/auth-lite/src/modules/domain/identity/identity-email.service.ts +34 -7
  113. package/templates/be/auth-lite/src/modules/domain/identity/persistence/account-doors.port.ts +28 -4
  114. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-refresh-token.entity.ts +4 -0
  115. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/index.ts +6 -1
  116. package/templates/be/auth-lite/src/modules/domain/identity/rate-counter.service.spec.ts +9 -2
  117. package/templates/be/auth-lite/src/modules/domain/identity/rate-counter.service.ts +11 -2
  118. package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.spec.ts +63 -10
  119. package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.ts +10 -2
  120. package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.spec.ts +25 -4
  121. package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.ts +8 -1
  122. package/templates/be/auth-lite/src/tests/fixtures/builders/identity.builder.ts +3 -1
  123. package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.ts +3 -3
  124. package/templates/be/skeleton/src/modules/platform/database/database.sql.ts +1 -1
  125. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +3 -2
  126. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +6 -2
  127. package/templates/be/skeleton/src/modules/platform/database/seed-runner.service.spec.ts +14 -9
  128. package/templates/be/skeleton/src/modules/platform/database/seed-runner.service.ts +3 -3
  129. package/templates/be/skeleton-lite/src/modules/platform/database/seed-runner.service.ts +32 -7
  130. package/templates/be/tool-config/jest.config.js +1 -1
  131. package/templates/fe/auth-lite/apps/web/src/modules/auth/email-doors.ts +4 -1
  132. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/EmailDoorForm/component.tsx +3 -0
  133. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/EmailDoorForm/index.tsx +14 -6
  134. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +1 -0
  135. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +1 -0
  136. package/templates/app/module/jest.config.js.tpl +0 -8
  137. /package/templates/app/skeleton/.starcistacks/dev/seeds/{primary-notes.sql → sql/primary-notes.sql} +0 -0
@@ -24,7 +24,7 @@ export declare function managedPathRegex(from: string): string;
24
24
  /**
25
25
  * Migrate a legacy `.sops.yaml` to the form `starci app secret init --env` writes (public recipients only, a rule per member spelling): a rule whose
26
26
  * path_regex matches the legacy `.enc` spelling alone also matches `.enc.yaml`, the spelling `secret set` writes, which sops otherwise refuses with
27
- * no creation rule. Recipients and the other rules are kept; nothing private is read or written.
27
+ * no creation rule. Recipients, the other rules and the comment header the file opens with (the managed header of `secret init`) are kept; nothing private is read or written.
28
28
  */
29
29
  export declare function migrateSopsRules({ root, write }: {
30
30
  root: string;
@@ -15,19 +15,24 @@ export function managedPathRegex(from) {
15
15
  return CURRENT;
16
16
  return /secrets/.test(from) && from.endsWith(LEGACY_END) ? `${from.slice(0, -LEGACY_END.length)}${MODERN_END}` : from;
17
17
  }
18
+ /** The comment lines a file opens with (its managed header among them), which a rewrite of the YAML body keeps as written. */
19
+ function leadingComments(text) {
20
+ return text.replace(/^((?:[ \t]*(?:#[^\n]*)?\r?\n)*)[\s\S]*$/, '$1');
21
+ }
18
22
  /**
19
23
  * Migrate a legacy `.sops.yaml` to the form `starci app secret init --env` writes (public recipients only, a rule per member spelling): a rule whose
20
24
  * path_regex matches the legacy `.enc` spelling alone also matches `.enc.yaml`, the spelling `secret set` writes, which sops otherwise refuses with
21
- * no creation rule. Recipients and the other rules are kept; nothing private is read or written.
25
+ * no creation rule. Recipients, the other rules and the comment header the file opens with (the managed header of `secret init`) are kept; nothing private is read or written.
22
26
  */
23
27
  export function migrateSopsRules({ root, write = false }) {
24
28
  const report = { schema: 'starci/sops-rules-report@1', root, write, file: FILE, changes: [], findings: [] };
25
29
  const target = path.join(root, FILE);
26
30
  if (!fs.existsSync(target))
27
31
  return report;
32
+ const text = fs.readFileSync(target, 'utf8');
28
33
  let config;
29
34
  try {
30
- config = parseYaml(fs.readFileSync(target, 'utf8'));
35
+ config = parseYaml(text);
31
36
  }
32
37
  catch {
33
38
  config = null;
@@ -46,7 +51,7 @@ export function migrateSopsRules({ root, write = false }) {
46
51
  rule.path_regex = to;
47
52
  });
48
53
  if (report.changes.length > 0 && write)
49
- fs.writeFileSync(target, stringifyYaml(config));
54
+ fs.writeFileSync(target, `${leadingComments(text)}${stringifyYaml(config)}`);
50
55
  return report;
51
56
  }
52
57
  const cell = (value) => String(value).replaceAll('|', '\\|').replaceAll('\n', ' ');
package/lint/run.d.mts CHANGED
@@ -15,8 +15,9 @@ export interface LinterRun {
15
15
  }
16
16
  /**
17
17
  * What a linter run is asked for. `list` is the files an ESLint run lints (cwd-relative, posix) when they are named one by one: they go to the child
18
- * through a file, never the command line, and `args` then holds the options alone. `files` is the number of source files the run judges, which the
19
- * heavy-command budget of the ESLint kind scales with (a list names its own count).
18
+ * through a file, never the command line, and `args` then holds the options alone. `files` is the number of source files the child's work is made of,
19
+ * which the heavy-command budget of the ESLint kind scales with: the typed program and the project graph of a side are built whole for a named list
20
+ * too, so a list run of a side whose scope is known names the side's count; without `files` a list names its own count.
20
21
  */
21
22
  export interface LinterSpec {
22
23
  cwd: string;
@@ -134,8 +135,8 @@ export interface CachedLinters {
134
135
  }
135
136
  /**
136
137
  * ESLint and stylelint through the result cache: per side, the files the records cannot vouch for go to the very launcher, chunking and result
137
- * validation of a full run (eslintRepository), the rest take their records. The answer has the shape of eslintRepository's, plus the stylelint
138
- * results, so the lint report is built the same way from either.
138
+ * validation of a full run (eslintRepository), the rest take their records; then the root packages, linted whole. Each scope runs on its own
139
+ * (scopeFailure). The answer has the shape of eslintRepository's, plus the stylelint results, so the lint report is built the same way from either.
139
140
  */
140
141
  export declare function cachedLinters({ repoRoot, app, side: only, run, session }: {
141
142
  repoRoot: string;
package/lint/run.mjs CHANGED
@@ -84,7 +84,7 @@ export async function runLinter({ cwd, pkg, bin, args, bound = null, list, files
84
84
  fs.writeFileSync(job, JSON.stringify({ files: list ?? ['.'], fix: args.includes('--fix'), ...(snapshotFile === null || graph === undefined ? {} : { graph: { out: snapshotFile, canons: graph } }) }));
85
85
  childArgs = [LIST_RUNNER, job];
86
86
  }
87
- const count = list?.length ?? files;
87
+ const count = files ?? list?.length;
88
88
  let run;
89
89
  let handed;
90
90
  try {
@@ -365,8 +365,10 @@ async function launchEslint({ app, repoRoot, opts, changed, existing, run = runL
365
365
  // the child that builds the canon's project graph for its rules hands the snapshot out when the caller keeps the graph findings (the cache); the packages are no side folder and have no graph
366
366
  const graph = handOutGraph && !packages ? graphCanonsOf(repoRoot, cwd, side) : undefined;
367
367
  // The files of a diff go to ONE child through a file (a diff of hundreds of long paths outgrows the Windows command line, and each extra
368
- // child would build the whole TypeScript program again); the whole side is a single directory argument.
369
- const launch = Promise.resolve().then(() => run(changed ? { cwd, pkg: 'eslint', bin: 'eslint', args: head, list: sources, bound: repoRoot, ...(graph === undefined ? {} : { graph }) } : { cwd, pkg: 'eslint', bin: 'eslint', args: [...head, packages ? PACKAGES : '.'], files: expected.length, bound: repoRoot, ...(graph === undefined ? {} : { graph }) }));
368
+ // child would build the whole TypeScript program again); the whole side is a single directory argument. Either child builds the typed program
369
+ // and the project graph of the whole side, so its liveness budget scales with the side's scope when the caller knows it, never with the diff alone.
370
+ const programFiles = known?.length ?? expected.length;
371
+ const launch = Promise.resolve().then(() => run(changed ? { cwd, pkg: 'eslint', bin: 'eslint', args: head, list: sources, files: programFiles, bound: repoRoot, ...(graph === undefined ? {} : { graph }) } : { cwd, pkg: 'eslint', bin: 'eslint', args: [...head, packages ? PACKAGES : '.'], files: programFiles, bound: repoRoot, ...(graph === undefined ? {} : { graph }) }));
370
372
  launches.push({ side, cwd, expected, run: launch.catch(spawnFailure) });
371
373
  }
372
374
  return launches;
@@ -489,10 +491,20 @@ export function gitBaseToken(repoRoot) {
489
491
  }
490
492
  return 'no-base';
491
493
  }
494
+ /**
495
+ * A scope whose linter reached no complete measurement (a child ended by its liveness budget, a capture cut short) is a tool error of that scope, and the
496
+ * next scope still runs: the root packages are judged whatever became of the back end. Nothing of the failed scope is recorded (its records are written
497
+ * only after its child completed). A hold of the host (HOST_HELD, HEAVY_DEFERRED) still ends the run, since no scope would be admitted either.
498
+ */
499
+ function scopeFailure(error, scope) {
500
+ if (!isNodeExecutionRefusal(error) || isHostHeld(error) || error.code === 'HEAVY_DEFERRED')
501
+ throw error;
502
+ return `eslint did not complete in ${scope}/: ${String(error.message).split('\n')[0]}`;
503
+ }
492
504
  /**
493
505
  * ESLint and stylelint through the result cache: per side, the files the records cannot vouch for go to the very launcher, chunking and result
494
- * validation of a full run (eslintRepository), the rest take their records. The answer has the shape of eslintRepository's, plus the stylelint
495
- * results, so the lint report is built the same way from either.
506
+ * validation of a full run (eslintRepository), the rest take their records; then the root packages, linted whole. Each scope runs on its own
507
+ * (scopeFailure). The answer has the shape of eslintRepository's, plus the stylelint results, so the lint report is built the same way from either.
496
508
  */
497
509
  export async function cachedLinters({ repoRoot, app, side: only, run, session }) {
498
510
  const eslint = { findings: [], errors: [], raw: [], engine: { sides: {} } };
@@ -514,23 +526,31 @@ export async function cachedLinters({ repoRoot, app, side: only, run, session })
514
526
  continue;
515
527
  }
516
528
  const styled = side === STYLE_SIDE;
517
- const outcome = await lintSide({
518
- session, side, sideRoot: cwd, scope, stylesheets: styled ? stylesheetsOf(cwd) : [],
519
- canons, styleCanon: styled ? await styleDeclaration({ repoRoot, cwd }) : null, typescript: () => loadAppTypeScript(cwd), extraKey: '', gitToken, full, write: true,
520
- eslint: async (files) => {
521
- const listed = files === null ? null : files.map((file) => `${side}/${file}`);
522
- const sub = await eslintRepository({ repoRoot, app, opts: { side }, changed: listed, existing: listed ?? [], run, scopes: { [side]: scope }, handOutGraph: true });
523
- return { results: sub.raw, errors: sub.errors, ...(sub.graphs[side] === undefined ? {} : { graph: sub.graphs[side] }) };
524
- },
525
- stylelint: styled ? async (files) => {
526
- const linted = await (run ?? runLinter)({ cwd, pkg: 'stylelint', bin: 'stylelint', args: [...(files ?? [STYLE_GLOB]), '--formatter', 'json'] });
527
- const appRelative = appRelativeMessages(STYLE_SIDE, cwd);
528
- for (const result of linted.results ?? [])
529
- for (const warning of result.warnings ?? [])
530
- warning.text = appRelative(warning.text);
531
- return { results: (linted.results ?? []), errors: linted.error ? [linted.error] : [] };
532
- } : null,
533
- });
529
+ let outcome;
530
+ try {
531
+ outcome = await lintSide({
532
+ session, side, sideRoot: cwd, scope, stylesheets: styled ? stylesheetsOf(cwd) : [],
533
+ canons, styleCanon: styled ? await styleDeclaration({ repoRoot, cwd }) : null, typescript: () => loadAppTypeScript(cwd), extraKey: '', gitToken, full, write: true,
534
+ eslint: async (files) => {
535
+ const listed = files === null ? null : files.map((file) => `${side}/${file}`);
536
+ const sub = await eslintRepository({ repoRoot, app, opts: { side }, changed: listed, existing: listed ?? [], run, scopes: { [side]: scope }, handOutGraph: true });
537
+ return { results: sub.raw, errors: sub.errors, ...(sub.graphs[side] === undefined ? {} : { graph: sub.graphs[side] }) };
538
+ },
539
+ stylelint: styled ? async (files) => {
540
+ const linted = await (run ?? runLinter)({ cwd, pkg: 'stylelint', bin: 'stylelint', args: [...(files ?? [STYLE_GLOB]), '--formatter', 'json'] });
541
+ const appRelative = appRelativeMessages(STYLE_SIDE, cwd);
542
+ for (const result of linted.results ?? [])
543
+ for (const warning of result.warnings ?? [])
544
+ warning.text = appRelative(warning.text);
545
+ return { results: (linted.results ?? []), errors: linted.error ? [linted.error] : [] };
546
+ } : null,
547
+ });
548
+ }
549
+ catch (error) {
550
+ eslint.errors.push(scopeFailure(error, side));
551
+ eslint.engine.sides[side] = { files: 0 };
552
+ continue;
553
+ }
534
554
  sides.push({ side, stats: outcome.stats });
535
555
  for (const message of outcome.errors)
536
556
  (message.startsWith('stylelint ') ? style.errors : eslint.errors).push(message);
@@ -544,11 +564,17 @@ export async function cachedLinters({ repoRoot, app, side: only, run, session })
544
564
  }
545
565
  // The root packages are small and have no result records: they are linted whole on every run, through the same launcher.
546
566
  if ((only === undefined || only === PACKAGES) && hasPackagesLint(repoRoot)) {
547
- const linted = await eslintRepository({ repoRoot, app, opts: { side: PACKAGES }, run });
548
- eslint.errors.push(...linted.errors);
549
- eslint.raw.push(...linted.raw);
550
- eslint.findings.push(...linted.findings);
551
- eslint.engine.sides[PACKAGES] = linted.engine.sides[PACKAGES] ?? { files: 0 };
567
+ try {
568
+ const linted = await eslintRepository({ repoRoot, app, opts: { side: PACKAGES }, run });
569
+ eslint.errors.push(...linted.errors);
570
+ eslint.raw.push(...linted.raw);
571
+ eslint.findings.push(...linted.findings);
572
+ eslint.engine.sides[PACKAGES] = linted.engine.sides[PACKAGES] ?? { files: 0 };
573
+ }
574
+ catch (error) {
575
+ eslint.errors.push(scopeFailure(error, PACKAGES));
576
+ eslint.engine.sides[PACKAGES] = { files: 0 };
577
+ }
552
578
  }
553
579
  session.report(lintFamilyReport(sides));
554
580
  return { eslint, stylelint: style, sides };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starci/hfs",
3
- "version": "5.3.0",
3
+ "version": "5.5.0",
4
4
  "description": "The StarCi app command implementation for scaffolding, linting, checking and synchronizing one product repository.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -5,7 +5,8 @@
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import { firstLine, tool } from '../stack/context.mjs';
8
- const LINT_SIDES = ['be', 'fe'];
8
+ import { PACKAGES, PACKAGES_CONFIG } from '../lint/run.mjs';
9
+ const LINT_SIDES = ['be', 'fe', PACKAGES];
9
10
  const MERGED_REPORT = 'reports/lint.sonar.json';
10
11
  const sideReport = (side) => `reports/lint.${side}.sonar.json`;
11
12
  /** CI merge: rules unique by id (last one wins, first position kept), issues concatenated in side order. */
@@ -14,8 +15,8 @@ function mergeSonarReports(reports) {
14
15
  const issues = reports.flatMap((report) => report.issues);
15
16
  return { rules, issues };
16
17
  }
17
- /** The sides the app has: a be/ or fe/ folder beside the app root. */
18
- const sidesOf = (root) => LINT_SIDES.filter((side) => fs.existsSync(path.join(root, side)));
18
+ /** The lint scopes the app has: a be/ or fe/ folder beside the app root, and its root packages when it has their managed config. */
19
+ const sidesOf = (root) => LINT_SIDES.filter((side) => fs.existsSync(path.join(root, side)) && (side !== PACKAGES || fs.existsSync(path.join(root, PACKAGES_CONFIG))));
19
20
  /**
20
21
  * Runs `starci app check --gate lint --side <side> --out <file>` for every side the app has, then writes reports/lint.sonar.json.
21
22
  * `launch` turns an argv into [command, args] for the platform. Returns { pass, text }: the honest log of every lint run and the verdict.
@@ -38,7 +38,7 @@ pins:
38
38
  source: packages/next-data/package.json
39
39
  why: 'the SWR data helpers of every front end (fe-stack data, owner:next-data-package): pollingRestart, the viewer and tenant key helpers, viewerSwrConfig and the viewer cache; each operation hook stays in the app and the package holds no component.'
40
40
  '@starci/grammar':
41
- version: 0.21.15
41
+ version: 0.21.17
42
42
  group: starci
43
43
  install: registry
44
44
  side: fe
@@ -52,28 +52,28 @@ pins:
52
52
  source: packages/next-i18n/package.json
53
53
  why: 'the next-intl stack of every front end (fe-stack i18n, owner:next-i18n-package): defineI18n over the locales of the app, the locale-negotiating proxy, the locale-aware navigation, the request config that loads the catalogs of the app and the locale read of the [locale] layout; the app keeps its message catalogs and writes none of the stack.'
54
54
  '@starci/eslint-canon-be':
55
- version: 3.14.0
55
+ version: 3.15.0
56
56
  group: starci
57
57
  install: registry
58
58
  side: be
59
59
  source: packages/eslint-canon-be/package.json
60
60
  why: 'the back-end ESLint canon: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; `starciBeConfig({ hfs: loadHfs(import.meta.url) })` is the typed factory and the BE-CONVENTION laws are its rules.'
61
61
  '@starci/eslint-canon-fe':
62
- version: 9.12.0
62
+ version: 9.13.0
63
63
  group: starci
64
64
  install: registry
65
65
  side: fe
66
66
  source: packages/eslint-canon-fe/package.json
67
67
  why: 'the front-end ESLint canon: `loadHfs(import.meta.url)` of fe/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the fe side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; the front end has no tests (FE_NO_TESTS R97 in hfs) and no-vietnamese-in-source (R91) applies.'
68
68
  '@starci/eslint-grammar-core':
69
- version: 0.21.15
69
+ version: 0.21.17
70
70
  group: starci
71
71
  install: registry
72
72
  side: fe
73
73
  source: packages/eslint-grammar-core/package.json
74
74
  why: 'the StarCi Grammar component API lint, versioned with @starci/grammar; front ends compose this family package with the universal FE canon.'
75
75
  '@starci/eslint-grammar-offset-pop':
76
- version: 0.21.15
76
+ version: 0.21.17
77
77
  group: starci
78
78
  install: registry
79
79
  side: fe
@@ -99,7 +99,7 @@ pins:
99
99
  side: both
100
100
  source: packages/prettier-config/package.json
101
101
  '@starci/jest-preset':
102
- version: 2.6.2
102
+ version: 2.7.0
103
103
  group: starci
104
104
  install: registry
105
105
  side: be
@@ -127,14 +127,14 @@ pins:
127
127
  source: packages/nest-kit/package.json
128
128
  why: 'the module definition of every back-end module: defineConfigurableModule and defineCapabilityModule return the module base class, the options token and the options types with their types stated, so a <x>.module-definition.ts is one call that the lint on stated export types (R368) and the duplicate-code check (R21) both accept; @starci/jest-preset compiles it as an ESM-only dependency.'
129
129
  '@starci/auth':
130
- version: 0.2.0
130
+ version: 0.3.0
131
131
  group: starci
132
132
  install: registry
133
133
  side: both
134
134
  source: packages/auth/package.json
135
135
  why: 'the lite sign-in: the NestJS module (EdDSA access tokens with a multi-key JWKS, passwords, Google OAuth with state and PKCE, rotating refresh sessions, the sign-in budget, the refresh cookie), the front-end client and its React bindings, the door contracts and test doubles; the one importer of the identity libraries, reached by a back end only through integrations/passport; @starci/jest-preset compiles it as an ESM-only dependency.'
136
136
  '@starci/auth-oidc':
137
- version: 0.1.0
137
+ version: 0.1.2
138
138
  group: starci
139
139
  install: registry
140
140
  side: both
@@ -155,7 +155,7 @@ pins:
155
155
  source: packages/mail/package.json
156
156
  why: 'outgoing mail of a back end: a NestJS module and the Mailer port over SMTP (connect, greeting, socket and total timeouts, STARTTLS required by default, recipient, message and attachment checks, a shared SMTP target policy, coded errors, typed escaping render helpers returning { subject, text, html }), with @starci/mail/testing for its specs; the only importer of nodemailer (R90).'
157
157
  '@starci/hfs':
158
- version: 5.3.0
158
+ version: 5.5.0
159
159
  group: starci
160
160
  install: registry
161
161
  side: both
@@ -592,7 +592,7 @@ pins:
592
592
  version: 3.1150.0
593
593
  group: framework
594
594
  side: be
595
- why: 'the S3 transport inside @starci/object-storage, the one ObjectStorage adapter of both editions (MinIO, S3, Spaces), which depends on it at exactly this version and which no app file imports; 3.1150.0 is the release of 2026-10-12, chosen over newer ones that had not aged two weeks.'
595
+ why: 'the S3 transport inside @starci/object-storage, the one ObjectStorage adapter of both editions (MinIO, S3, Spaces), which depends on it at exactly this version and which no app file imports; 3.1150.0 is the release of 2026-10-12.'
596
596
  '@aws-sdk/s3-request-presigner':
597
597
  version: 3.1150.0
598
598
  group: framework
@@ -602,7 +602,7 @@ pins:
602
602
  version: 10.1.7
603
603
  group: framework
604
604
  side: be
605
- why: 'the SMTP client inside @starci/mail, which depends on it at exactly this version and which no app file imports; 10.1.7 is the release of 2026-10-18, chosen over newer ones that had not aged two weeks.'
605
+ why: 'the SMTP client inside @starci/mail, which depends on it at exactly this version and which no app file imports; 10.1.7 is the release of 2026-10-18.'
606
606
  '@apollo/server':
607
607
  version: 5.5.1
608
608
  group: framework
@@ -652,7 +652,7 @@ pins:
652
652
  version: 6.29.0
653
653
  group: framework
654
654
  side: be
655
- why: 'the Agent of platform/http (its one owner, R90): FetchHttpClient hands fetch a pool that trusts HttpOptions.trustedCa beside the bundled roots (a test world''s per-run fake CA); the 6 line is the undici the Node 22 runtime of the app images bundles, and its Agent also serves the fetch of Node 24; 6.29.0 is the release of 2026-09-25, chosen over newer ones that had not aged two weeks.'
655
+ why: 'the Agent of platform/http (its one owner, R90): FetchHttpClient hands fetch a pool that trusts HttpOptions.trustedCa beside the bundled roots (a test world''s per-run fake CA); the 6 line is the undici the Node 22 runtime of the app images bundles, and its Agent also serves the fetch of Node 24; 6.29.0 is the release of 2026-09-25.'
656
656
  # --- lite identity (integrations/passport, ruling lite-self-hosted-postgres-passport-minio)
657
657
  '@nestjs/passport':
658
658
  version: 11.0.5
@@ -77,7 +77,7 @@ items:
77
77
  check: ci-standard
78
78
  - id: sonar-lint-per-side
79
79
  title: the Sonar lint runs per side
80
- requires: "a lint with a Sonar import (`starci app check --gate lint --out`) names its side (--side be|fe); the reports are merged into reports/lint.sonar.json afterwards; no combined lint with Sonar"
80
+ requires: "a lint with a Sonar import (`starci app check --gate lint --out`) names its side (--side be|fe|packages); the reports are merged into reports/lint.sonar.json afterwards; no combined lint with Sonar"
81
81
  covers: [app-ci, runtime]
82
82
  check: ci-standard
83
83
  - id: test-sharding
@@ -87,7 +87,7 @@ items:
87
87
  check: ci-standard
88
88
  - id: lint-parallel
89
89
  title: lint each side in its own job
90
- requires: "the lint job is a matrix over side [be, fe]; each side lints (with its own Sonar report when a token is set) in parallel, and the sonar job merges the reports"
90
+ requires: "the lint job is a matrix over side [be, fe], plus packages in an app with root packages; each scope lints (with its own Sonar report when a token is set) in parallel and on its own, and the sonar job merges the reports"
91
91
  covers: [app-ci]
92
92
  check: ci-standard
93
93
  - id: cache-saved-always
@@ -3299,7 +3299,7 @@ rules:
3299
3299
  code: "HFS_DOCKER_BUILD_CONTEXT"
3300
3300
  editions: [lite, full]
3301
3301
  docs: [docs/application-stacks.md#r187]
3302
- law: "Every app image is built from the app root: the header comment of an app's Dockerfile states `starci docker build <app>` for its own path, and no COPY or ADD source leaves the build context (a `..` segment or an absolute path)."
3302
+ law: "Every app image is built from the app root: the header comment of an app's Dockerfile states `starci docker build <app>` for its own path, no COPY or ADD source leaves the build context (a `..` segment or an absolute path), and the image of a Next app whose front end reads be/contracts/ copies that folder exactly when the app holds it (a COPY of a missing folder fails the build, a missing COPY leaves the codegen without its contract)."
3303
3303
  kinds: [check]
3304
3304
  gates: [settle, land, ci]
3305
3305
  failureCodes: ["HFS_DOCKER_BUILD_CONTEXT"]
@@ -5791,7 +5791,7 @@ rules:
5791
5791
  ruling: "Lite storage is the MinIO already running on the Tay Son host, reached through the one S3 integration."
5792
5792
  - id: R332
5793
5793
  code: "LITE_DB_MIGRATION"
5794
- law: "A lite app's schema lives only in db/: every schema change is one db/migrations/<ts14>_<kebab>.sql (archive db/migrations/archive/, seed db/seed.sql), and the tree holds no supabase/ folder and no SQL file outside db/ but a .starcistacks seed."
5794
+ law: "A lite app's schema lives only in db/: every schema change is one db/migrations/<ts14>_<kebab>.sql (archive db/migrations/archive/, the managed db/bootstrap.sql), every other SQL file under db/ is refused, and the tree holds no supabase/ folder and no SQL file outside db/ but the sql/ entries of its seed levels (R375)."
5795
5795
  kinds: [check]
5796
5796
  gates: [settle, land, ci]
5797
5797
  failureCodes: ["LITE_DB_MIGRATION"]
@@ -6628,3 +6628,16 @@ rules:
6628
6628
  - {path: contexts/project/stacks.md#project-stacks-11-a-customised-service-is-an-image-the-app-builds-the-same-in-every-environment, editions: [lite, full]}
6629
6629
  proves:
6630
6630
  - {path: packages/hfs/src/stacks-service-images.spec.mjs, editions: [lite, full]}
6631
+ - id: R375
6632
+ code: "HFS_SEEDS_SHAPE"
6633
+ law: "Seed data lives only in the seed levels of `.starcistacks/`, where `seeds/` is applied in every environment first and `<env>/seeds/` in the one environment the stack declaration names `<env>`; each level is a `seeds.yaml` manifest (schema starci/seeds@1) whose entries list every file of its `sql/`, `data/` and `files/` folders; a seed is never a migration (no statement that creates, alters, drops, truncates, grants, revokes or comments on an object), never custody, a data document validates against the schema its entry names, a level a production environment applies holds no test data but the one test account an environment declares (a test-account entry with its reason), and a document names a secret only as a reference to a sealed seeds-<name> member of its environment."
6634
+ kinds: [check]
6635
+ gates: [settle, land, ci]
6636
+ failureCodes: ["HFS_SEEDS_SHAPE", "HFS_SEED_MANIFEST", "HFS_SEED_DDL", "HFS_SEED_DATA_SCHEMA", "HFS_SEED_TEST_DATA", "HFS_SEED_SECRET_REF"]
6637
+ enforcers:
6638
+ - {kind: hfs, id: seeds-shape, at: scripts/hfs/rules/seeds.mjs}
6639
+ editions: [lite, full]
6640
+ docs:
6641
+ - {path: contexts/project/stacks.md#project-stacks-12-seed-data-lives-in-the-seed-levels-of-the-stack, editions: [lite, full]}
6642
+ proves:
6643
+ - {path: packages/hfs/src/hfs-seeds-shape.spec.mjs, editions: [lite, full]}
@@ -7,7 +7,7 @@
7
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@3
10
- version: 3.17.0
10
+ version: 3.19.0
11
11
 
12
12
  versioning:
13
13
  # MAJOR.MINOR.PATCH of this manifest. A repository pins only the MAJOR in hfs.json ("hfs": 3).
@@ -61,6 +61,10 @@ versioning:
61
61
  # 3.17.0 (ruling auth-oidc-package, 2026-11-03) adds the optional full-edition be.integrations.keycloak, the glue of @starci/auth-oidc (the OIDC client of the
62
62
  # identity provider behind the PrincipalVerifier of @starci/auth), and gives @starci/auth-oidc to it in ruleParams.be.infraOwners.
63
63
  # 3.17.0 (ruling module-packages, 2026-11-03) adds the optional app.packages and app.module-registry: the workspace packages of the app root (the agent SDK, the module packages, the managed registry).
64
+ # 3.18.0 (ruling seed-levels, 2026-11-03) gives app.starcistacks its two seed levels, the shared .starcistacks/seeds/ and each environment's
65
+ # <env>/seeds/, each a seeds.yaml manifest and the folders sql/, data/ and files/, and retires app.db.seed (its rows are the shared level's
66
+ # sql/ entries); `starci app codemod seeds-layout` moves an older app's seed data into them.
67
+ # 3.19.0 (ruling root-packages-any-name, 2026-11-03) widens app.packages to any packages/<name>/: the agent SDK, a block library, a module package.
64
68
  # 3.15.0 (ruling app-check-cache, 2026-10-31) adds the optional app.starcistacks.cache, the committed check cache of the app (.starcistacks/cache/, both editions).
65
69
  # 3.14.0 (ruling app-secrets-one-standard, 2026-10-29) adds the optional app.ci-stack-key-restore, the managed workflow that writes the
66
70
  # deploy server's age identity back from its GitHub Environment backup.
@@ -502,7 +506,7 @@ ruleParams:
502
506
  # R334 (lite): the only `.query(` outside platform/database's composer and the test world is the pass-through of a file listed here
503
507
  # (or in hfs.json sides.be.sqlPassThrough), whose SQL text is not the app's own. Paths are relative to the side folder.
504
508
  sqlPassThrough:
505
- - {file: src/modules/platform/database/seed-runner.service.ts, reason: "the tracked seed file's text, run as it is (db/seed.sql); the app writes no SQL there"}
509
+ - {file: src/modules/platform/database/seed-runner.service.ts, reason: "the tracked seed files' text, run as it is (the sql/ entries of .starcistacks/seeds/, then of .starcistacks/<env>/seeds/); the app writes no SQL there"}
506
510
 
507
511
  lite:
508
512
  schemaAuthority: sql
@@ -891,10 +895,10 @@ slots:
891
895
  - id: app.packages
892
896
  since: 3.17.0
893
897
  profiles: [app]
894
- editionWhy: Lite and full share the slot; a product's agent SDK and its agent modules are workspace packages in both.
895
- # A workspace package of the app ROOT (packages/agent-sdk, packages/module-<name>): first-party source that both sides import through its
898
+ editionWhy: Lite and full share the slot; every root workspace package of a product (its agent SDK, a block library, a module package, a reviewed plugin package) is one in both.
899
+ # A workspace package of the app ROOT (packages/<name>: the agent SDK, a block library, packages/module-<name>): first-party source that both sides import through its
896
900
  # package entry points and never past them (ARCH_INTERNAL_IMPORT_OUTSIDE), linted by the root eslint config by subpath.
897
- path: "packages/{agent-sdk,module-<name>}/"
901
+ path: "packages/<name>/"
898
902
  presence: optional
899
903
  tracked: tracked
900
904
  tier: none
@@ -964,16 +968,18 @@ slots:
964
968
  tests: none
965
969
  requires: [application-stacks.yaml]
966
970
  allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/infra/{compose,swarm,k8s,terraform}/**", "<env>/infra/metadata.json",
967
- "<env>/runtime/{config,env}/**", "<env>/runtime/env/KEYS.md", "<env>/secrets/<slug>.{enc.yaml,enc}", "<env>/secrets/.gitkeep", "<env>/seeds/**"]
971
+ "<env>/runtime/{config,env}/**", "<env>/runtime/env/KEYS.md", "<env>/secrets/<slug>.{enc.yaml,enc}", "<env>/secrets/.gitkeep",
972
+ "seeds/seeds.yaml", "seeds/{sql,data,files}/**", "<env>/seeds/seeds.yaml", "<env>/seeds/{sql,data,files}/**"]
968
973
  forbids: ["<env>/runtime/files/**", "**/*.enc outside <env>/secrets/", DESIGN.md, deployment.json, "k8s/ at root"]
969
- # Owner ruling 2026-10-14: lite declares docker-swarm production, app runtime config, sealed custody and seed data.
974
+ # Owner ruling 2026-10-14: lite declares docker-swarm production, app runtime config, sealed custody and the seed levels.
970
975
  # The db service supplies the database; schedules are platform/jobs ticks (R335); production has no Compose or host cron.
971
976
  lite:
972
977
  requires: [application-stacks.yaml]
973
978
  allows: [application-stacks.yaml, images/db/Dockerfile, "<env>/README.md", "<env>/environment.json", "<env>/infra/{compose,swarm}/**", "<env>/infra/metadata.json",
974
- "<env>/runtime/{config,env}/**", "<env>/runtime/env/KEYS.md", "<env>/secrets/<slug>.{enc.yaml,enc}", "<env>/secrets/.gitkeep", "<env>/seeds/**"]
979
+ "<env>/runtime/{config,env}/**", "<env>/runtime/env/KEYS.md", "<env>/secrets/<slug>.{enc.yaml,enc}", "<env>/secrets/.gitkeep",
980
+ "seeds/seeds.yaml", "seeds/{sql,data,files}/**", "<env>/seeds/seeds.yaml", "<env>/seeds/{sql,data,files}/**"]
975
981
  forbids: ["<env>/runtime/files/**", "**/*.enc outside <env>/secrets/", DESIGN.md, deployment.json, "k8s/ at root"]
976
- rules: [HFS_STACKS_SHAPE, HFS_STACKS_IN_SIDE, HFS_PLAINTEXT_SECRET, HFS_IDENTITY_CUSTODY]
982
+ rules: [HFS_STACKS_SHAPE, HFS_STACKS_IN_SIDE, HFS_PLAINTEXT_SECRET, HFS_IDENTITY_CUSTODY, HFS_SEEDS_SHAPE, HFS_SEED_MANIFEST, HFS_SEED_DDL, HFS_SEED_DATA_SCHEMA, HFS_SEED_TEST_DATA, HFS_SEED_SECRET_REF, HFS_SEED_PII]
977
983
  - id: app.sops
978
984
  since: 3.1.0
979
985
  profiles: [app]
@@ -1072,15 +1078,6 @@ slots:
1072
1078
  tier: none
1073
1079
  tests: none
1074
1080
  since: 3.9.0
1075
- - id: app.db.seed
1076
- profiles: [app]
1077
- editions: [lite]
1078
- path: "db/seed.sql"
1079
- presence: optional # synthetic dev data only; a lite app seeds nothing it cannot regenerate
1080
- tracked: tracked
1081
- tier: none
1082
- tests: none
1083
- since: 3.9.0
1084
1081
  - id: app.db.image
1085
1082
  profiles: [app]
1086
1083
  editions: [lite]
@@ -4233,6 +4233,67 @@ HFS_SEED_PII:
4233
4233
  owner: op-retry
4234
4234
  kind: check-finding
4235
4235
 
4236
+ HFS_SEEDS_SHAPE:
4237
+ title: "Seed data lives in a seed level of .starcistacks and holds no custody"
4238
+ title_vi: "Dữ liệu seed nằm ngoài cấp seed hoặc chứa custody"
4239
+ meaning_vi: "Một tệp dưới `.starcistacks/seeds/` hoặc `.starcistacks/<env>/seeds/` không phải `seeds.yaml` và không nằm trong `sql/`, `data/`, `files/`; hoặc thuộc cấp seed của một môi trường không được khai báo; hoặc là custody (envelope `.enc`, khoá `.pem`/`.key`, tệp `.env`)."
4240
+ causes_vi:
4241
+ - "Vi phạm R375: dữ liệu seed chỉ nằm trong hai cấp seed của `.starcistacks/`, mỗi cấp là một manifest và các thư mục sql/, data/, files/."
4242
+ - "Một secret được đặt cạnh seed thay vì niêm phong bằng sops trong `.starcistacks/<env>/secrets/`."
4243
+ nextStep_vi: "Chuyển tệp vào thư mục loại của cấp seed (`starci app codemod seeds-layout` chuyển dữ liệu cũ); niêm phong secret bằng `starci app secret set --env <env>` và xoá bản rõ."
4244
+ owner: op-retry
4245
+ kind: check-finding
4246
+
4247
+ HFS_SEED_MANIFEST:
4248
+ title: "Every seed file is an entry of its level's seeds.yaml"
4249
+ title_vi: "Manifest seeds.yaml thiếu, sai hoặc không liệt kê tệp seed"
4250
+ meaning_vi: "Cấp seed có tệp nhưng không có `seeds.yaml`, manifest không đọc được hoặc sai schema starci/seeds@1, một mục trỏ tới đường dẫn không tồn tại, `after` nêu id không có hoặc tạo vòng, hay một tệp trong sql/, data/, files/ không thuộc mục nào."
4251
+ causes_vi:
4252
+ - "Vi phạm R375: manifest là nơi khai báo thứ tự, loại, đích và chế độ (once, upsert) của mọi seed."
4253
+ nextStep_vi: "Thêm hoặc sửa mục `{ id, kind, path, target, mode, after? }` trong `seeds.yaml` của cấp đó cho tệp được nêu."
4254
+ owner: op-retry
4255
+ kind: check-finding
4256
+
4257
+ HFS_SEED_DDL:
4258
+ title: "A seed only writes rows: seed data is never a migration"
4259
+ title_vi: "Seed chứa câu lệnh thay đổi schema"
4260
+ meaning_vi: "Một tệp SQL của mục sql có câu lệnh CREATE, ALTER, DROP, TRUNCATE, GRANT, REVOKE hoặc COMMENT: thay đổi schema hoặc quyền, việc chỉ migration được làm."
4261
+ causes_vi:
4262
+ - "Vi phạm R375 và BE-PERSISTENCE-1: dữ liệu seed không bao giờ là migration, và migration là nơi duy nhất thay đổi schema."
4263
+ nextStep_vi: "Chuyển câu lệnh schema thành một migration (lite: `db/migrations/<ts14>_<kebab>.sql`), để seed chỉ ghi dòng dữ liệu một cách idempotent (`on conflict do nothing` hoặc upsert)."
4264
+ owner: op-retry
4265
+ kind: check-finding
4266
+
4267
+ HFS_SEED_DATA_SCHEMA:
4268
+ title: "A data seed validates against the schema its entry names"
4269
+ title_vi: "Tài liệu seed không khớp schema đã khai báo"
4270
+ meaning_vi: "Một tài liệu JSON hoặc YAML của mục data không qua được JSON schema mà mục đó nêu (`schema`, do app khai báo), hoặc schema đó không đọc được."
4271
+ causes_vi:
4272
+ - "Vi phạm R375: app sở hữu ý nghĩa và schema của dữ liệu, harness kiểm tra tài liệu trước khi lệnh của app nạp nó."
4273
+ nextStep_vi: "Sửa tài liệu cho khớp schema, hoặc sửa đường dẫn `schema` của mục trỏ tới schema app khai báo."
4274
+ owner: op-retry
4275
+ kind: check-finding
4276
+
4277
+ HFS_SEED_TEST_DATA:
4278
+ title: "A level production applies holds no test data"
4279
+ title_vi: "Seed áp dụng cho production chứa dữ liệu thử"
4280
+ meaning_vi: "Cấp seed dùng chung hoặc cấp của một môi trường không phải docker-compose có danh tính example.test hoặc giá trị, tên tệp bắt đầu bằng uat- hay test- ngoài mục test-account duy nhất mà môi trường đó khai báo."
4281
+ causes_vi:
4282
+ - "Vi phạm R375: dữ liệu thử (tài khoản mẫu, workspace mẫu) chỉ nằm trong cấp seed của môi trường dev."
4283
+ nextStep_vi: "Chuyển dữ liệu thử vào `.starcistacks/dev/seeds/`, hoặc khai báo tài khoản thử duy nhất của production là một mục `kind: test-account` có `reason`; chỉ để dữ liệu ban đầu thật (gói, danh mục) ở cấp dùng chung."
4284
+ owner: op-retry
4285
+ kind: check-finding
4286
+
4287
+ HFS_SEED_SECRET_REF:
4288
+ title: "A seed names a secret only as a sealed seeds-<name> member of its environment"
4289
+ title_vi: "Tham chiếu secret của seed sai tên hoặc thiếu member niêm phong"
4290
+ meaning_vi: "Một tài liệu seed có `{ \"secret\": \"<member>\" }` mà tên không có dạng seeds-<name>, hoặc môi trường của cấp đó (cấp dùng chung: mọi môi trường) không có `.starcistacks/<env>/secrets/<member>.enc.yaml`."
4291
+ causes_vi:
4292
+ - "Vi phạm R375: giá trị bí mật của seed nằm trong custody sops của môi trường, được bước seed mở trong bộ nhớ, không bao giờ nằm trong tệp seed."
4293
+ nextStep_vi: "Niêm phong giá trị bằng `starci app secret set --env <env>` vào member seeds-<name> của môi trường đó và tham chiếu đúng tên."
4294
+ owner: op-retry
4295
+ kind: check-finding
4296
+
4236
4297
  HFS_SERVICE_PLACEMENT:
4237
4298
  title: "A back-end service lives only at be/apps/<service>"
4238
4299
  title_vi: "Dịch vụ back end nằm ngoài be/apps/<service>"
@@ -5668,10 +5729,10 @@ BE_STORAGE_OWNER:
5668
5729
  LITE_DB_MIGRATION:
5669
5730
  title: "A lite schema change lives outside db/migrations"
5670
5731
  title_vi: "Thay đổi schema của app lite nằm ngoài db/migrations"
5671
- meaning_vi: "`<path>` là tệp của thư mục supabase/ hoặc một tệp SQL ngoài db/ (trừ seed của .starcistacks)."
5732
+ meaning_vi: "`<path>` là tệp của thư mục supabase/, một tệp SQL ngoài db/ (trừ mục sql của các cấp seed trong `.starcistacks/`), hoặc một tệp SQL trong db/ không phải migration, archive hay bootstrap."
5672
5733
  causes_vi:
5673
- - "Vi phạm R332: schema của app lite chỉ nằm trong db/migrations/<ts14>_<kebab>.sql, archive và seed trong db/."
5674
- nextStep_vi: "Chuyển migration sang db/migrations (giữ nguyên stamp), seed sang db/seed.sql, types sang db/types/; xoá supabase/."
5734
+ - "Vi phạm R332: schema của app lite chỉ nằm trong db/migrations/<ts14>_<kebab>.sql, archive và db/bootstrap.sql; dữ liệu seed nằm trong các cấp seed của `.starcistacks/` (R375)."
5735
+ nextStep_vi: "Chuyển migration sang db/migrations (giữ nguyên stamp), seed sang `.starcistacks/seeds/sql/` (`starci app codemod seeds-layout`), types sang db/types/; xoá supabase/."
5675
5736
  owner: op-retry
5676
5737
  kind: check-finding
5677
5738
 
@@ -1,4 +1,4 @@
1
- // root-packages.mjs - the workspace packages of an app ROOT (packages/agent-sdk, packages/module-<name>, the managed packages/modules).
1
+ // root-packages.mjs - the workspace packages of an app ROOT (packages/<name>: the agent SDK, a block library, a module package, the managed packages/modules).
2
2
  // They lie outside both side folders and belong to no side; both sides import them by package name, through their package entry points
3
3
  // (the `exports` of the manifest), so the import graph reads them as first-party packages and the back-end canon never reads them as
4
4
  // a third-party library. The app root's one package.json lists them in `workspaces`; the patterns that start with a side folder
@@ -18,6 +18,9 @@
18
18
  // HFS_MIN_INSTANCES fewer instances of a slot than minInstances
19
19
  // HFS_PLAINTEXT_SECRET (R06, rules/secrets.mjs) a plaintext secret file or value in a tracked file; a `.enc` that is no sops envelope
20
20
  // HFS_STACKS_SHAPE (R10, rules/stacks.mjs) invalid stack shape, Swarm runtime policy, host crontab installation or lite backing duplication
21
+ // HFS_SEEDS_SHAPE, HFS_SEED_MANIFEST, HFS_SEED_DDL, HFS_SEED_DATA_SCHEMA, HFS_SEED_TEST_DATA, HFS_SEED_SECRET_REF (R375, rules/seeds.mjs) seed
22
+ // data outside the seed levels or unlisted, custody, a schema statement, a document off its schema, test data
23
+ // production applies outside its one test account, a secret reference to no sealed seeds-<name> member
21
24
  // HFS_CI_MISSING_CANON (R13, rules/pipeline.mjs) CI without the pinned `starci app check`, pre-push without typecheck or lint
22
25
  // HFS_DEP_VERSION_SKEW (R14, rules/deps.mjs) a dependency at two versions in the workspace, or a nested copy in the lockfile
23
26
  // HFS_CONTRACT_SNAPSHOT_DRIFT (R23, rules/contract.mjs) an uncommitted back-end snapshot, or a front-end copy that differs from it
@@ -108,7 +111,7 @@ export const CHECK_CODES = Object.freeze([
108
111
  'HFS_SLOT_UNDECLARED', 'HFS_SLOT_NOT_ENABLED', 'HFS_SLOT_AMBIGUOUS', 'HFS_TRACKED_MUST_BE_IGNORED', 'HFS_FORBIDDEN_PRESENT',
109
112
  'HFS_SLOT_REQUIRED_MISSING', 'HFS_MIN_INSTANCES', 'HFS_CANON_PIN_DRIFT', 'HFS_SIZE_SOFT_BACKLOG', 'BE_SOURCE_FORM',
110
113
  'HFS_MANAGED_FILE_DRIFT', 'HFS_TOOL_CONFIG_LOCAL', 'HFS_RULE_OFF_WITHOUT_REPLACEMENT', 'HFS_TS_STRICT',
111
- 'HFS_PLAINTEXT_SECRET', 'HFS_STACKS_SHAPE', 'HFS_CI_MISSING_CANON', 'HFS_DEP_VERSION_SKEW', 'HFS_CONTRACT_SNAPSHOT_DRIFT', 'HFS_MCP_TOOLS_SNAPSHOT',
114
+ 'HFS_PLAINTEXT_SECRET', 'HFS_STACKS_SHAPE', 'HFS_SEEDS_SHAPE', 'HFS_SEED_MANIFEST', 'HFS_SEED_DDL', 'HFS_SEED_DATA_SCHEMA', 'HFS_SEED_TEST_DATA', 'HFS_SEED_SECRET_REF', 'HFS_CI_MISSING_CANON', 'HFS_DEP_VERSION_SKEW', 'HFS_CONTRACT_SNAPSHOT_DRIFT', 'HFS_MCP_TOOLS_SNAPSHOT',
112
115
  'BE_TEST_TOPOLOGY', 'BE_SPEC_PLACEMENT', 'HFS_REPO_LOCAL_CHECK', 'HFS_LINT_SUPPRESSION_FILE', 'HFS_BYPASS_MARKER', 'HFS_PROOF_COMMAND_FILE_MISSING', 'HFS_PEER_INTEGRATION_MISSING', 'BE_INTEGRATION_SPEC_MISSING', 'FE_GRAPHQL_CONTRACT', 'FE_NO_TESTS', 'HFS_MONO_WORKSPACES', 'HFS_MONO_FE_WORKSPACE', 'HFS_MONO_NEST_PROJECTS', 'HFS_MONO_WORKSPACE_DEP', 'BE_CLI_REQUIRED', 'FE_WIRE_GENERATED', 'FE_I18N_PLACEMENT', 'FE_I18N_CATALOG', 'HFS_SERVICE_PLACEMENT', 'HFS_IMAGE_UNPINNED', 'HFS_SERVICE_STACK_DECLARATION', 'HFS_EVENT_CONTRACT', 'BE_ASYNC_SPEC_MISSING', 'BE_SAGA_STEP_COMPENSATION', 'BE_SAGA_STATE_VERSIONED', 'BE_SAGA_EVENT_CONTRACT', 'BE_SAGA_CONSUMER_DEDUPE', 'BE_EVENT_CLASS_CONTRACT', 'BE_PATTERN_SPEC_MISSING', 'BE_CONTRACT_BREAKING', 'BE_KIND_DECLARATION', 'BE_KIND_EMPTY', 'HFS_DOCKER_BUILD_CONTEXT', 'HFS_DOCKER_STAGES', 'HFS_DOCKER_ENTRY', 'HFS_DOCKER_BASE_PIN', 'HFS_DOCKER_SECRETS', 'BE_SAGA_E2E_MISSING',
113
116
  'FE_STACK_FORBIDDEN', 'HFS_EDITION_FORBIDDEN_PRESENT', 'HFS_BACKING_UNDECLARED', 'HFS_BACKING_UNUSED', 'HFS_LITE_SECRET_CUSTODY', 'HFS_STARCIWORK_LITE_FORBIDDEN',
114
117
  'DB_MIGRATION_SHAPE', 'DB_RLS_REQUIRED', 'DB_DYNAMIC_DDL', 'DB_POLICY_SHAPE', 'DB_DEFINER_SAFE', 'DB_STORAGE_POLICY', 'LITE_DB_MIGRATION',
@@ -84,4 +84,19 @@ export declare function measuredBy({ roots, roles, files, excludes }: {
84
84
  }): (file: string) => boolean;
85
85
  /** True when the be-relative `file` is measured by the scope of the whole manifest (measuredBy). */
86
86
  export declare const isMeasured: (manifest: ScopeManifest, file: string) => boolean;
87
+ /** The measured scope of the app's root packages, every path app-relative: the folders that hold specs, the files measured and the ones never measured. */
88
+ export interface PackagesCoverage {
89
+ folders: string[];
90
+ files: string[];
91
+ excludes: string[];
92
+ }
93
+ /**
94
+ * THE derivation of what the root packages measure (BE-TEST-13): every TypeScript file below a package's `src/` but its front-end subtrees and barrels, at
95
+ * 100 per file in the back end's unit run. The rendered be/jest.config.js, Sonar's coverage exclusions and sources, Codecov's paths and R273 read it.
96
+ */
97
+ export declare function packagesCoverage(folders: readonly string[]): PackagesCoverage;
98
+ /** The app-relative folders of the root packages a tracked file list holds: each `packages/<name>/` with a `package.json` and a `src/`, the registry aside. */
99
+ export declare function rootPackageFoldersOf(files: readonly string[]): string[];
100
+ /** The one test of whether an app-relative file is measured by a packages scope (a spec never is): R273 asks it. */
101
+ export declare function packageMeasuredBy({ files, excludes }: PackagesCoverage): (file: string) => boolean;
87
102
  export {};