@starci/hfs 4.7.47 → 4.9.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 (153) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +4 -9
  3. package/emit/operation-schemas.mjs +3 -0
  4. package/lint/bound-sys.cjs +2 -2
  5. package/package.json +11 -5
  6. package/runtime/engine/admission.mjs +1 -1
  7. package/runtime/engine/config.mjs +2 -2
  8. package/runtime/engine/db/machine.mjs +2 -3
  9. package/runtime/engine/invalid-config.mjs +1 -1
  10. package/runtime/knowledge/grammars/{starci → core}/playbook.yaml +3 -3
  11. package/runtime/knowledge/grammars/index.yaml +14 -23
  12. package/runtime/knowledge/grammars/offset-pop/playbook.yaml +2 -2
  13. package/runtime/knowledge/hfs/canon-pins.yaml +23 -23
  14. package/runtime/knowledge/hfs/fe-stack.yaml +1 -1
  15. package/runtime/knowledge/hfs/rules.yaml +83 -60
  16. package/runtime/modules/kernel/failure-codes.yaml +23 -13
  17. package/runtime/scripts/guards/git-policy.mjs +2 -3
  18. package/runtime/scripts/hfs/rule-catalog.mjs +2 -2
  19. package/runtime/scripts/hfs/rules/pipeline.mjs +5 -5
  20. package/runtime/scripts/lib/example-refs.mjs +10 -3
  21. package/runtime/scripts/lib/grammar-lint.mjs +16 -3
  22. package/runtime/scripts/lib/package-at.mjs +5 -6
  23. package/runtime/scripts/lib/roots.mjs +2 -2
  24. package/runtime/scripts/work/draw/draw-dna-findings.mjs +23 -12
  25. package/runtime/scripts/work/draw/draw-dna.mjs +14 -8
  26. package/scaffold/app.mjs +6 -6
  27. package/src/main.mjs +3 -3
  28. package/sync/hygiene.mjs +1 -1
  29. package/sync/index.mjs +15 -2
  30. package/sync/managed.mjs +3 -2
  31. package/templates/app/codegen-graphql.mjs.partial +4 -4
  32. package/templates/app/quality-config/sonar-project.properties +4 -2
  33. package/runtime/engine/canonical-json.mjs +0 -14
  34. package/runtime/engine/db/ledger-open.mjs +0 -38
  35. package/runtime/engine/db/ledger-paths.mjs +0 -87
  36. package/runtime/engine/db/ledger.mjs +0 -1068
  37. package/runtime/engine/db/provision-authority.mjs +0 -76
  38. package/runtime/engine/db/provision-operations.mjs +0 -146
  39. package/runtime/packages/grammar/scripts/build-stamp.mjs +0 -98
  40. package/runtime/scripts/agent/codex-app-server.mjs +0 -79
  41. package/runtime/scripts/agent/codex-mirror-source.mjs +0 -34
  42. package/runtime/scripts/agent/host-agents.mjs +0 -80
  43. package/runtime/scripts/agent/launch-trust-policy.mjs +0 -53
  44. package/runtime/scripts/agent/trust-launch.mjs +0 -191
  45. package/runtime/scripts/agent/trust.mjs +0 -523
  46. package/runtime/scripts/api/fs/temp-path.mjs +0 -8
  47. package/runtime/scripts/api/git/diff-names.mjs +0 -8
  48. package/runtime/scripts/api/git/head-time.mjs +0 -10
  49. package/runtime/scripts/api/git/index-lock.mjs +0 -78
  50. package/runtime/scripts/api/git/log.mjs +0 -5
  51. package/runtime/scripts/api/git/merge-tree.mjs +0 -5
  52. package/runtime/scripts/api/git/rev-list.mjs +0 -5
  53. package/runtime/scripts/api/git/rev-parse.mjs +0 -13
  54. package/runtime/scripts/api/git/status-query.mjs +0 -5
  55. package/runtime/scripts/api/git/worktree-list-query.mjs +0 -5
  56. package/runtime/scripts/api/node/spawn-node.mjs +0 -6
  57. package/runtime/scripts/api/npm/dist-tag.mjs +0 -8
  58. package/runtime/scripts/api/npm/lib.mjs +0 -32
  59. package/runtime/scripts/api/npm/pack-dry-run.mjs +0 -9
  60. package/runtime/scripts/api/npm/run-npm.mjs +0 -6
  61. package/runtime/scripts/api/npm/view.mjs +0 -30
  62. package/runtime/scripts/api/npm/whoami.mjs +0 -8
  63. package/runtime/scripts/api/process/hide-child-windows.mjs +0 -31
  64. package/runtime/scripts/api/process/kill-tree.mjs +0 -22
  65. package/runtime/scripts/api/process/lib.mjs +0 -125
  66. package/runtime/scripts/api/process/probe-guard-command.mjs +0 -74
  67. package/runtime/scripts/api/process/process-list.mjs +0 -28
  68. package/runtime/scripts/api/process/system-tool.mjs +0 -37
  69. package/runtime/scripts/gates/canon-digest.mjs +0 -81
  70. package/runtime/scripts/gates/canon-pins.mjs +0 -119
  71. package/runtime/scripts/gates/gate-input.mjs +0 -98
  72. package/runtime/scripts/gates/gate.mjs +0 -636
  73. package/runtime/scripts/gates/grammar-dist.mjs +0 -206
  74. package/runtime/scripts/gates/output.mjs +0 -40
  75. package/runtime/scripts/gates/runtime-package-selection.mjs +0 -27
  76. package/runtime/scripts/gates/test-world-run.mjs +0 -252
  77. package/runtime/scripts/gates/type-impact.mjs +0 -277
  78. package/runtime/scripts/gates/type-program.mjs +0 -113
  79. package/runtime/scripts/guards/assigned-command.mjs +0 -12
  80. package/runtime/scripts/guards/call-rights.mjs +0 -47
  81. package/runtime/scripts/guards/command-guard.mjs +0 -468
  82. package/runtime/scripts/guards/command-policy.mjs +0 -342
  83. package/runtime/scripts/guards/critic-reach.mjs +0 -82
  84. package/runtime/scripts/guards/deps-guard.mjs +0 -150
  85. package/runtime/scripts/guards/env-dump-verdict.mjs +0 -89
  86. package/runtime/scripts/guards/guards-root.mjs +0 -27
  87. package/runtime/scripts/guards/hook-install.mjs +0 -343
  88. package/runtime/scripts/guards/hook-verdict.mjs +0 -12
  89. package/runtime/scripts/guards/install-verdict.mjs +0 -41
  90. package/runtime/scripts/guards/kernel-seat.mjs +0 -63
  91. package/runtime/scripts/guards/launch-verdict.mjs +0 -74
  92. package/runtime/scripts/guards/op-starci.mjs +0 -16
  93. package/runtime/scripts/guards/op-write-scope.mjs +0 -27
  94. package/runtime/scripts/guards/orca-self-lifecycle.mjs +0 -39
  95. package/runtime/scripts/guards/process-kill-verdict.mjs +0 -106
  96. package/runtime/scripts/guards/protected-zone.mjs +0 -56
  97. package/runtime/scripts/guards/refusals.mjs +0 -27
  98. package/runtime/scripts/guards/rights.mjs +0 -268
  99. package/runtime/scripts/guards/seat-tools.mjs +0 -47
  100. package/runtime/scripts/guards/shell-commands.mjs +0 -328
  101. package/runtime/scripts/guards/supervisor-seat.mjs +0 -80
  102. package/runtime/scripts/guards/verdict-process.mjs +0 -81
  103. package/runtime/scripts/lib/app-data.mjs +0 -11
  104. package/runtime/scripts/lib/canon-binding-text.mjs +0 -129
  105. package/runtime/scripts/lib/check-format.mjs +0 -7
  106. package/runtime/scripts/lib/failure-code-reads.mjs +0 -100
  107. package/runtime/scripts/lib/failure-code-template-errors.mjs +0 -161
  108. package/runtime/scripts/lib/git-dir.mjs +0 -120
  109. package/runtime/scripts/lib/guard-command.mjs +0 -24
  110. package/runtime/scripts/lib/interrupt-cleanup.mjs +0 -38
  111. package/runtime/scripts/lib/json-formats.mjs +0 -34
  112. package/runtime/scripts/lib/managed-json.mjs +0 -51
  113. package/runtime/scripts/lib/menu-parts.mjs +0 -11
  114. package/runtime/scripts/lib/op-gate.mjs +0 -15
  115. package/runtime/scripts/lib/provision-control-failure.mjs +0 -32
  116. package/runtime/scripts/lib/release-version.mjs +0 -134
  117. package/runtime/scripts/lib/runtime-location.mjs +0 -21
  118. package/runtime/scripts/lib/runtime-package-inventory.mjs +0 -21
  119. package/runtime/scripts/lib/runtime-package-manifests.mjs +0 -84
  120. package/runtime/scripts/lib/runtime-publication-tags.mjs +0 -56
  121. package/runtime/scripts/lib/seat-env.mjs +0 -43
  122. package/runtime/scripts/lib/sonar-new-code.mjs +0 -58
  123. package/runtime/scripts/lib/source-package-imports.mjs +0 -111
  124. package/runtime/scripts/lib/tar-files.mjs +0 -39
  125. package/runtime/scripts/lib/uat-run-folder.mjs +0 -118
  126. package/runtime/scripts/lib/worktree-record.mjs +0 -14
  127. package/runtime/scripts/machine/ask-receipts.mjs +0 -113
  128. package/runtime/scripts/machine/evidence-store.mjs +0 -223
  129. package/runtime/scripts/machine/home.mjs +0 -216
  130. package/runtime/scripts/machine/host-lock.mjs +0 -184
  131. package/runtime/scripts/machine/lock-recovery.mjs +0 -55
  132. package/runtime/scripts/machine/roles-contract.mjs +0 -90
  133. package/runtime/scripts/machine/runtime-change.mjs +0 -20
  134. package/runtime/scripts/machine/workflow-tree.mjs +0 -122
  135. package/runtime/scripts/reconciler/checklist-items.mjs +0 -7
  136. package/runtime/scripts/reconciler/guard-row.mjs +0 -25
  137. package/runtime/scripts/work/brand/brand-colour.mjs +0 -125
  138. package/runtime/scripts/work/brand/brand-tokens.mjs +0 -167
  139. package/runtime/scripts/work/brand/brand-value-source.mjs +0 -106
  140. package/runtime/scripts/work/brand/brand.mjs +0 -892
  141. package/runtime/scripts/work/draw/draw-layer.mjs +0 -306
  142. package/runtime/scripts/work/draw/draw-rationale-page.mjs +0 -237
  143. package/runtime/scripts/work/draw/draw-rationale-ref.mjs +0 -14
  144. package/runtime/scripts/work/draw/draw-rationale.mjs +0 -353
  145. package/runtime/scripts/work/draw/draw-source.mjs +0 -412
  146. package/runtime/scripts/work/draw/draw-taste.mjs +0 -278
  147. package/runtime/scripts/work/draw/draw-typecheck.mjs +0 -72
  148. package/runtime/scripts/work/draw/rationale-findings.mjs +0 -205
  149. package/runtime/scripts/work/draw-grammar.mjs +0 -175
  150. package/runtime/scripts/work/draw-render-page.mjs +0 -188
  151. package/runtime/scripts/work/draw-render.mjs +0 -626
  152. package/runtime/scripts/work/png.mjs +0 -237
  153. package/runtime/scripts/work/render-tools.mjs +0 -64
@@ -13,9 +13,9 @@ export const packageAt = (dir, name) => {
13
13
  /** This runtime's own `starci` bin (packages/cli is the only package that declares one). */
14
14
  const RUNTIME_STARCI_BIN = path.join(skillRoot, 'packages', 'cli', 'bin', 'starci.mjs');
15
15
 
16
- /** The `starci` bin of the @starci/cli that `dir` installs (a product runs its own pinned CLI), else this runtime's own. */
16
+ /** The `starci` bin of the starci-harness that `dir` installs (a product runs its own pinned CLI), else this runtime's own. */
17
17
  function starciBin(dir) {
18
- const packageFile = packageAt(dir, '@starci/cli');
18
+ const packageFile = packageAt(dir, 'starci-harness');
19
19
  if (packageFile) {
20
20
  try {
21
21
  const { bin } = JSON.parse(fs.readFileSync(packageFile, 'utf8'));
@@ -50,13 +50,12 @@ export const loadTypescript = (...dirs) => {
50
50
  };
51
51
 
52
52
  /**
53
- * The app's command line and the app implementation behind it: `bin` the `starci` bin of the app's installed @starci/cli, else
53
+ * The app's command line and the app implementation behind it: `bin` the `starci` bin of the app's installed starci-harness, else
54
54
  * this runtime's packages/cli/bin/starci.mjs (starciBin); `dir` the @starci/hfs that answers
55
- * `starci app` - the app's own install, else the one beside its @starci/cli, else this runtime's packages/hfs - whose runtime
56
- * slice the gate's base lint (scripts/gates/gate.mjs) and the READ digest (scripts/gates/read-digest.mjs) load in-process.
55
+ * `starci app` - the app's own install, else the one beside its starci-harness, else this runtime's packages/hfs.
57
56
  */
58
57
  export function hfsEntry(root) {
59
- const cli = packageAt(root, '@starci/cli');
58
+ const cli = packageAt(root, 'starci-harness');
60
59
  const manifest = packageAt(root, '@starci/hfs') ?? (cli && packageAt(path.dirname(cli), '@starci/hfs'));
61
60
  return { dir: manifest ? path.dirname(manifest) : path.join(skillRoot, 'packages', 'hfs'), bin: starciBin(root) };
62
61
  }
@@ -11,8 +11,8 @@
11
11
  // product trees treesInOrder the workflow tree first, then the product's main checkout
12
12
  // Work records workRecordPath the first tree that holds the record, else the main checkout's path
13
13
  // tool lookups toolSearchDirs the tree the work happens in first, the runtime's own install last
14
- // The workflow tree itself, a job's placement and the Critic directory are registry facts: scripts/machine/workflow-tree.mjs,
15
- // placement-rebound.mjs and the worktree registry resolve them, and a caller hands the result in as `tree`.
14
+ // The workflow tree itself, a job's placement and the Critic directory are registry facts: the worktree registry resolves
15
+ // them, and a caller hands the result in as `tree`.
16
16
  // The `root-adhoc` self-check (scripts/checks/check-root-adhoc.mjs, rule R240) refuses a new path built from the process working
17
17
  // directory or from the Work directory name outside this module.
18
18
  import fs from 'node:fs';
@@ -12,7 +12,7 @@ export function appendDnaFindings({ all, dna, proposals, label, assetRequests, t
12
12
  alertFindings(all, dna, rules, add, helpers);
13
13
  meterTrackFindings(all, rules, add, helpers);
14
14
  artworkFindings(all, assetRequests, add, helpers);
15
- badgeDotFindings(all, rules, add, helpers);
15
+ statusDotFindings(all, rules, add, helpers);
16
16
  return groupedFindings(groups, label);
17
17
  }
18
18
 
@@ -221,29 +221,40 @@ function artworkFindingFor(el, assetRequests, add, h) {
221
221
  if (assetRequests && !assetRequests.has(id)) add(h.DRAW_ASSET_SLOT_UNDECLARED, 'asset slot without a request', slotEl, `${h.ASSET_SLOT_ATTR}="${id}" has no entry in the drawing's asset-request.md`);
222
222
  }
223
223
 
224
- function badgeDotFindings(all, rules, add, h) {
225
- for (const el of all) badgeDotFindingFor(el, rules, add, h);
224
+ function statusDotFindings(all, rules, add, h) {
225
+ for (const el of all) statusDotFindingFor(el, rules, add, h);
226
226
  }
227
227
 
228
- function badgeDotFindingFor(el, rules, add, h) {
228
+ /** The nearest declaration of the dot-size token on the element or an ancestor, in px; null when none is declared. */
229
+ function statusDotTokenPx(el, rules, h) {
230
+ for (const node of [el, ...h.ancestorsOf(el)]) {
231
+ const px = h.declaredPx(node, h.STATUS_DOT_SIZE_PROPERTY, rules);
232
+ if (px != null) return px;
233
+ }
234
+ return null;
235
+ }
236
+
237
+ function statusDotFindingFor(el, rules, add, h) {
229
238
  if (h.insideSvg(el)) return;
230
239
  const part = (el.attrs[h.PART_ATTR] ?? '').trim();
240
+ const slot = (el.attrs['data-slot'] ?? '').trim();
231
241
  const cls = h.classesOf(el);
232
- const dotLike = /(^|-)dot$/.test(part) || cls.some((c) => h.DOT_CLASS.test(c));
242
+ const inDotComponent = h.inComponent(el, h.COMPONENTS_WITH_DOT);
243
+ if (inDotComponent && (h.STATUS_DOT_ANATOMY.test(part) || cls.some((c) => h.STATUS_DOT_ANATOMY.test(c)))) return;
244
+ const dotLike = /(^|-)dot(-mark)?$/.test(part) || cls.some((c) => h.DOT_CLASS.test(c));
233
245
  if (!dotLike) return;
234
- const dnaDot = ['badge-dot', 'dot'].includes(part) || cls.includes(h.BADGE_DOT_CLASS);
235
- const inBadge = h.inComponent(el, ['Badge']);
236
- if (!dnaDot || !inBadge) {
237
- add(h.DRAW_OFF_GRAMMAR_COMPONENT, 'hand-made status dot', el, `a status dot is DNA Badge isDot (data-grammar-part="badge-dot" / ${h.BADGE_DOT_CLASS} inside a Badge), never a hand-drawn dot`);
246
+ const dnaDot = [part, slot].includes(h.STATUS_DOT_MARK_PART) || cls.includes(h.STATUS_DOT_MARK_CLASS);
247
+ if (!dnaDot || !inDotComponent) {
248
+ add(h.DRAW_OFF_GRAMMAR_COMPONENT, 'hand-made status dot', el, `a status dot is the DNA StatusDot mark (data-grammar-part="${h.STATUS_DOT_MARK_PART}" / ${h.STATUS_DOT_MARK_CLASS} inside a StatusDot or a Badge isDot), never a hand-drawn dot`);
238
249
  return;
239
250
  }
240
251
  const inline = h.declsOf(el.attrs.style);
241
252
  const ruled = rules.filter((r) => h.selectorMatches(r.selector, el)).map((r) => r.decls);
242
253
  const ring = [inline, ...ruled].some((d) => ['box-shadow', 'outline', 'border'].some((key) => d[key] && !/^(none|0|0px)$/i.test(d[key].trim())))
243
254
  || cls.some((c) => /^(ring|shadow|outline)(-|$)/.test(c) && !/^(ring|shadow|outline)-none$/.test(c));
244
- if (ring) add(h.DRAW_OFF_GRAMMAR_COMPONENT, 'Badge dot with a halo', el, 'the Badge dot is a plain 6px solid circle in the tone colour: no box-shadow, ring, outline or border halo');
245
- const px = Math.max(h.declaredPx(el, 'width', rules) ?? 0, h.declaredPx(el, 'height', rules) ?? 0, Number(el.attrs.width) || 0, Number(el.attrs.height) || 0);
246
- if (px && Math.abs(px - h.BADGE_DOT_PX) > 0.5) add(h.DRAW_OFF_GRAMMAR_COMPONENT, 'Badge dot off its size', el, `the dot declares ${px}px: the Badge dot is ${h.BADGE_DOT_PX}px (<CircleFill width={6} />), no size variants`);
255
+ if (ring) add(h.DRAW_OFF_GRAMMAR_COMPONENT, 'status dot with a halo', el, `the status dot is a plain ${h.STATUS_DOT_PX}px solid circle in the tone colour: no box-shadow, ring, outline or border halo (the echo ring is the StatusDot's own)`);
256
+ const px = Math.max(h.declaredPx(el, 'width', rules) ?? 0, h.declaredPx(el, 'height', rules) ?? 0, Number(el.attrs.width) || 0, Number(el.attrs.height) || 0, statusDotTokenPx(el, rules, h) ?? 0);
257
+ if (px && Math.abs(px - h.STATUS_DOT_PX) > 0.5) add(h.DRAW_OFF_GRAMMAR_COMPONENT, 'status dot off its size', el, `the dot declares ${px}px: the status dot is ${h.STATUS_DOT_PX}px, read from ${h.STATUS_DOT_SIZE_PROPERTY}, no size variants`);
247
258
  }
248
259
 
249
260
  function groupedFindings(groups, label) {
@@ -17,9 +17,10 @@
17
17
  // inherits its parent's; an svg's inner shapes inherit the svg's); a component name DNA
18
18
  // does not publish; a part none of the enclosing components publishes; a closed prop
19
19
  // value outside its DNA values; a proposal with no entry in a grammar-proposal file;
20
- // a status dot that is not the DNA Badge isDot dot (grammar 0.5.3: HeroUI's
21
- // <CircleFill width={6} />, part badge-dot / class starci-core-badge-dot inside a Badge) -
22
- // a hand-made dot span, a dot with a box-shadow/ring/outline halo, or a dot not 6px;
20
+ // a status dot that is not the DNA StatusDot mark (grammar 0.14: class
21
+ // starci-core-status-dot-mark / part status-dot-mark inside a StatusDot or a Badge isDot,
22
+ // sized by --starci-core-status-dot-size, 6px) - a hand-made dot span, a dot with a
23
+ // box-shadow/ring/outline halo, or a dot (or a redefined size token) not 6px;
23
24
  // DRAW_NOTICE_NOT_ALERT a notice that is not DNA `Alert` with a tone: an Alert without a tone in the
24
25
  // PresentationState vocabulary, or a container (SurfaceCard, a bare div ...) posing as a
25
26
  // notice - role alert/status or aria-live, notice/callout/banner vocabulary in its
@@ -67,9 +68,14 @@ export { anatomyFindings } from './draw-dna-anatomy.mjs';
67
68
  export const DRAW_OFF_GRAMMAR_COMPONENT = 'DRAW_OFF_GRAMMAR_COMPONENT';
68
69
  export const DRAW_NOTICE_NOT_ALERT = 'DRAW_NOTICE_NOT_ALERT';
69
70
  export const DRAW_RATIO_NOT_METER = 'DRAW_RATIO_NOT_METER';
70
- /** Grammar 0.5.3 Badge isDot: the dot's class and its one size (HeroUI's <CircleFill width={6} />). */
71
- const BADGE_DOT_CLASS = 'starci-core-badge-dot';
72
- const BADGE_DOT_PX = 6;
71
+ /** Grammar 0.14 StatusDot (also behind Badge isDot): the dot's class, part, size custom property and its one value. */
72
+ const STATUS_DOT_MARK_CLASS = 'starci-core-status-dot-mark';
73
+ const STATUS_DOT_MARK_PART = 'status-dot-mark';
74
+ const STATUS_DOT_SIZE_PROPERTY = '--starci-core-status-dot-size';
75
+ const STATUS_DOT_PX = 6;
76
+ /** The StatusDot's own anatomy around the mark (root, chip, label): parts of the component, never a dot. */
77
+ const STATUS_DOT_ANATOMY = /^(starci-core-)?status-dot(-chip|-label)?$/;
78
+ const COMPONENTS_WITH_DOT = ['StatusDot', 'Badge'];
73
79
  const DOT_CLASS = /(?:^|[-_])(dot|status-dot|dot-indicator)(?:$|[-_])/i;
74
80
  /**
75
81
  * The grammar Alert's action Button variant for a PresentationState tone (@starci/grammar 0.5.3 Alert, after HeroUI v3's
@@ -425,8 +431,8 @@ function isArtwork(el) {
425
431
  * its asset-request.md requests (null: not judged).
426
432
  */
427
433
  const DNA_FINDING_HELPERS = {
428
- ACTION_COMPONENTS, ALERT_INDICATOR_MAX_PX, ASSET_SLOT_ATTR, BADGE_DOT_CLASS, BADGE_DOT_PX, BAR_CLASS, CONTAINERS,
429
- DOT_CLASS, DRAW_ALERT_ANATOMY, DRAW_ASSET_SLOT_UNDECLARED, DRAW_METER_TRACK, DRAW_NOTICE_NOT_ALERT, DRAW_OFF_GRAMMAR_COMPONENT, DRAW_RATIO_NOT_METER,
434
+ ACTION_COMPONENTS, ALERT_INDICATOR_MAX_PX, ASSET_SLOT_ATTR, BAR_CLASS, COMPONENTS_WITH_DOT, CONTAINERS,
435
+ DOT_CLASS, STATUS_DOT_ANATOMY, STATUS_DOT_MARK_CLASS, STATUS_DOT_MARK_PART, STATUS_DOT_PX, STATUS_DOT_SIZE_PROPERTY, DRAW_ALERT_ANATOMY, DRAW_ASSET_SLOT_UNDECLARED, DRAW_METER_TRACK, DRAW_NOTICE_NOT_ALERT, DRAW_OFF_GRAMMAR_COMPONENT, DRAW_RATIO_NOT_METER,
430
436
  METER_FULL_WIDTH_SHARE, METER_SEGMENTED_TRACK_PX, METER_SEGMENTS_PROPOSAL, METER_TRACK_PX, METER_SEGMENT_GAP_MAX_PX, NOTICE_CLASS, OUTCOME_STATES,
431
437
  PART_ATTR, PROPOSAL_ATTR, RATIO_RX, alertActionVariantFor, ancestorsOf, classesOf, componentNameOf, componentRootOf,
432
438
  declaredBackgroundsOf, declaredPx, declsOf, describe, dnaSegment, inComponent, insideSvg, isAction, isArtwork, kebab,
package/scaffold/app.mjs CHANGED
@@ -127,7 +127,7 @@ export const packageJsonText = value => `${JSON.stringify(value, null, 2)}\n`;
127
127
  const STARTER_SIDES = Object.freeze({
128
128
  be: Object.freeze({ apps: [{ name: 'core', kind: 'api' }, { name: 'cli', kind: 'cli' }], kinds: ['api', 'cli'], connections: [{ name: 'primary', envPrefix: 'PRIMARY_DB', owner: 'core', isolation: 'database' }] }),
129
129
  // fe: the landing and the product app, over the shared ui and i18n packages (both opt-in slots, enabled here).
130
- fe: Object.freeze({ grammar: 'starci', apps: [{ name: 'landing', kind: 'next' }, { name: 'app', kind: 'next' }], reads: ['be/contracts/'], optionalSlots: ['fe.package.ui', 'fe.package.i18n'] }),
130
+ fe: Object.freeze({ grammar: 'core', apps: [{ name: 'landing', kind: 'next' }, { name: 'app', kind: 'next' }], reads: ['be/contracts/'], optionalSlots: ['fe.package.ui', 'fe.package.i18n'] }),
131
131
  });
132
132
 
133
133
  /** The lite starter is the upgrade-safe subset: one API, one web app and one Supabase schema authority. */
@@ -138,7 +138,7 @@ const LITE_STARTER_SIDES = Object.freeze({
138
138
  connections: [{ name: 'primary', envPrefix: 'PRIMARY_DB', owner: 'api', isolation: 'schema', provider: 'supabase' }],
139
139
  reads: ['supabase/types/'],
140
140
  }),
141
- fe: Object.freeze({ grammar: 'starci', apps: [{ name: 'web', kind: 'next' }], reads: ['be/contracts/', 'supabase/types/'] }),
141
+ fe: Object.freeze({ grammar: 'core', apps: [{ name: 'web', kind: 'next' }], reads: ['be/contracts/', 'supabase/types/'] }),
142
142
  });
143
143
 
144
144
  /** The checked local Supabase auth posture of every newly scaffolded lite app. */
@@ -161,8 +161,8 @@ const STARTER_DEPENDENCIES = Object.freeze({
161
161
  'class-transformer': null, 'class-validator': null, 'reflect-metadata': null, rxjs: null, tslib: null, typeorm: null, zod: null,
162
162
  },
163
163
  devDependencies: {
164
- turbo: null, '@nestjs/testing': null, '@starci/cli': null, '@starci/eslint-canon-be': null, '@starci/eslint-canon-fe': null, '@starci/jest-preset': null,
165
- '@starci/prettier-config': null, '@starci/stylelint-canon': null, '@starci/test-world': null, '@starci/tsconfig': null, '@tailwindcss/postcss': null, '@types/express': null,
164
+ turbo: null, '@nestjs/testing': null, '@starci/eslint-canon-be': null, '@starci/eslint-canon-fe': null, '@starci/jest-preset': null,
165
+ '@starci/prettier-config': null, '@starci/stylelint-canon': null, '@starci/test-world': null, '@starci/tsconfig': null, 'starci-harness': null, '@tailwindcss/postcss': null, '@types/express': null,
166
166
  '@types/jest': null, '@types/node': null, '@types/react': null, '@types/react-dom': null, eslint: null, 'eslint-plugin-react-hooks': null,
167
167
  husky: null, jest: null, 'postcss-value-parser': null, prettier: null, stylelint: null, tailwindcss: null, 'ts-jest': null,
168
168
  'ts-node-dev': null, 'tsc-alias': null, 'tsconfig-paths': null, typescript: null,
@@ -182,8 +182,8 @@ const LITE_STARTER_DEPENDENCIES = Object.freeze({
182
182
  'reflect-metadata': null, rxjs: null, tslib: null, typeorm: null, zod: null,
183
183
  },
184
184
  devDependencies: {
185
- turbo: null, '@nestjs/testing': null, '@starci/eslint-canon-be': null, '@starci/eslint-canon-fe': null, '@starci/cli': null,
186
- '@starci/jest-preset': null, '@starci/prettier-config': null, '@starci/stylelint-canon': null, '@starci/tsconfig': null, '@tailwindcss/postcss': null,
185
+ turbo: null, '@nestjs/testing': null, '@starci/eslint-canon-be': null, '@starci/eslint-canon-fe': null,
186
+ '@starci/jest-preset': null, '@starci/prettier-config': null, '@starci/stylelint-canon': null, '@starci/tsconfig': null, 'starci-harness': null, '@tailwindcss/postcss': null,
187
187
  '@types/express': null, '@types/jest': null, '@types/node': null, '@types/react': null, '@types/react-dom': null,
188
188
  eslint: null, 'eslint-plugin-react-hooks': null, husky: null, jest: null, 'postcss-value-parser': null, prettier: null,
189
189
  stylelint: null, supabase: null, tailwindcss: null, 'ts-jest': null, 'ts-node-dev': null, 'tsc-alias': null,
package/src/main.mjs CHANGED
@@ -136,7 +136,7 @@ starci app hygiene [--cwd <dir>]
136
136
  starci app coverage merge --out <lcov> <lcov-or-dir>... [--cwd <dir>]
137
137
  `;
138
138
  const PER_CODE_LIMIT = 25;
139
- const VALUE_FLAGS = new Set(['--base', '--inject', '--into', '--event', '--from', '--service', '--connection', '--owner', '--failed', '--done', '--coverage-from', '--registry-visibility']);
139
+ const VALUE_FLAGS = new Set(['--base', '--inject', '--into', '--event', '--from', '--service', '--connection', '--owner', '--failed', '--done', '--coverage-from', '--registry-visibility', '--shapes', '--read', '--app']);
140
140
  /** Flags that may repeat: their values are collected in order. */
141
141
  const LIST_FLAGS = new Set(['--inject']);
142
142
  const BOOL_FLAGS = new Set(['--json', '--fast', '--fe', '--no-types', '--db-types', '--wiring']);
@@ -293,7 +293,7 @@ async function lintMain(argv, { cwd, stdout, presets, prettier, runLinter, check
293
293
 
294
294
  /**
295
295
  * The jest preset a new app renders its Sonar exclusions from: the copy this @starci/hfs ships (bundledPresets), the same version the
296
- * scaffold pins, so `npx @starci/cli app scaffold` works before any dependency of the app is installed.
296
+ * scaffold pins, so `npx starci-harness app scaffold` works before any dependency of the app is installed.
297
297
  */
298
298
  const scaffoldPresets = () => bundledPresets();
299
299
 
@@ -390,7 +390,7 @@ function newMain(opts, { cwd: repoRoot, stdout: out }) {
390
390
  function addMain(opts, { cwd: repoRoot, stdout: out }) {
391
391
  const [noun, name, ...extra] = opts.positional;
392
392
  if (!noun || !name || extra.length) throw new Error('starci app add takes `<noun> <name>` and the options of the noun');
393
- const { created, registered } = addKind({ repoRoot, noun, name, options: { event: opts.event, from: opts.from, service: opts.service, connection: opts.connection, owner: opts.owner, failed: opts.failed, done: opts.done, fe: opts.fe, noTypes: opts['no-types'] } });
393
+ const { created, registered } = addKind({ repoRoot, noun, name, options: { event: opts.event, from: opts.from, service: opts.service, connection: opts.connection, owner: opts.owner, failed: opts.failed, done: opts.done, fe: opts.fe, noTypes: opts['no-types'], shapes: opts.shapes, read: opts.read, app: opts.app } });
394
394
  for (const file of created) out(`created ${file}\n`);
395
395
  const kinds = registered.kinds.length ? `; kinds ${registered.kinds.join(', ')}` : '';
396
396
  out(`starci app add ${noun}: registered patterns ${registered.patterns.join(', ')}${kinds}\n`);
package/sync/hygiene.mjs CHANGED
@@ -126,7 +126,7 @@ export function judge(cwd, files) {
126
126
 
127
127
  // The scripts/housekeeping/ledger-hygiene.mjs of the checkout under judgment: the root git names for `cwd`, which carries
128
128
  // that script only when the repository IS a full StarCi runtime checkout (packages/hfs/sync/ sits 3 directories
129
- // under such a root, the same computation scripts/hfs/sync-runtime.mjs uses). A product repository — or a
129
+ // under such a root). A product repository — or a
130
130
  // bare repo — has no such file, so the section is silently absent wherever this module happens to be installed.
131
131
  const ledgerHygieneScript = cwd => {
132
132
  try {
package/sync/index.mjs CHANGED
@@ -176,7 +176,7 @@ export async function loadPresets(root) {
176
176
 
177
177
  /**
178
178
  * The jest preset's Sonar exclusions as @starci/hfs ships them: `@starci/jest-preset/sonar-exclusions.cjs` (a require-free module),
179
- * copied byte for byte into packages/hfs/runtime by scripts/hfs/sync-runtime.mjs, so a fresh `starci app scaffold` (nothing installed
179
+ * carried byte for byte in packages/hfs/runtime, so a fresh `starci app scaffold` (nothing installed
180
180
  * yet) and an upgrade render the same list the app's installed preset answers. A missing copy is a refusal, never a render without it.
181
181
  */
182
182
  const BUNDLED_SONAR_EXCLUSIONS = new URL('../runtime/packages/jest-preset/sonar-exclusions.cjs', import.meta.url);
@@ -200,6 +200,18 @@ const LCOV_REPORT = 'be/coverage/lcov.info';
200
200
  */
201
201
  export const SONAR_CPD_EXCLUSIONS = Object.freeze(['**/messages/*.messages.ts', '**/messages/*.json']);
202
202
 
203
+ /**
204
+ * The Sonar scope of the jest preset's exclusions (presets.sonarExclusions): its file patterns are the tests, which
205
+ * `sonar.test.inclusions` takes and `sonar.exclusions` moves out of the sources (`sonar.tests` names every source folder, so the
206
+ * two sets stay disjoint and each test is analysed as a test, never dropped); its folder patterns (`/**`) and the front end's
207
+ * build and dependency folders are no source at all. Nothing else is excluded: a generated file is analysed like any other.
208
+ */
209
+ export function sonarScope(presets) {
210
+ const preset = String(presets?.sonarExclusions ?? '').split(',').map(glob => glob.trim()).filter(Boolean);
211
+ const tests = preset.filter(glob => !glob.endsWith('/**'));
212
+ return { tests, exclusions: [...new Set([...preset, '**/.next/**', '**/node_modules/**'])] };
213
+ }
214
+
203
215
  const SOURCE_FILE = /\.[cm]?ts$/;
204
216
  const SOURCE_SKIPPED = new Set(['node_modules', 'dist', 'coverage']);
205
217
 
@@ -364,7 +376,8 @@ export function variables(app, scope, presets, sonarKey, manifest = loadSlotMani
364
376
  nodeMajor: String(NODE_MAJOR),
365
377
  sonarKey: sonarKey ?? app.project,
366
378
  sonarHostLine: sonarHostLine(quality.sonar),
367
- sonarExclusions: [presets?.sonarExclusions, '**/.next/**', '**/node_modules/**', ...(lite ? ['supabase/types/**'] : [])].filter(Boolean).join(','),
379
+ sonarExclusions: sonarScope(presets).exclusions.join(','),
380
+ sonarTestInclusions: sonarScope(presets).tests.join(','),
368
381
  sonarSources: ['be/apps', 'be/src', 'fe/apps', ...(packages ? ['fe/packages'] : [])].join(','),
369
382
  lcovReport: LCOV_REPORT,
370
383
  cpdExclusions: SONAR_CPD_EXCLUSIONS.join(','),
package/sync/managed.mjs CHANGED
@@ -23,8 +23,9 @@
23
23
  // a file that states the coverage scope no longer states what the slot manifest derives (the `coverage` field of
24
24
  // knowledge/hfs/slots.yaml through scripts/hfs/coverage-scope.mjs): be/jest.config.js, codecov.yml, or the
25
25
  // `sonar.coverage.exclusions` line of sonar-project.properties. The scope is never edited by hand.
26
- // HFS_SONAR_CONFIG (R11) sonar-project.properties differs from its render (the render names no host URL, sources and tests that
27
- // do not overlap, the `sonar.exclusions` of the jest preset (a back end), the be lcov with the logic of be/src/modules as the coverage scope, the ESLint report and the
26
+ // HFS_SONAR_CONFIG (R11) sonar-project.properties differs from its render (the render names no host URL, the source folders as
27
+ // `sonar.tests` too, the jest preset's test patterns as `sonar.test.inclusions` moved out of the sources by
28
+ // `sonar.exclusions` beside the build and dependency folders (sync/index.mjs sonarScope), the be lcov with the logic of be/src/modules as the coverage scope, the ESLint report and the
28
29
  // HFS import files Sonar reads), or the stack declaration names a quality gate other than the one
29
30
  // gate of knowledge/sonar-gate.yaml (bundled in the runtime copy). One edit is one finding.
30
31
  // HFS_TS_STRICT (R22) the root tsconfig.json drift (either profile), named by flag (ts-strict.mjs) instead of by hash.
@@ -28,12 +28,12 @@ const writeFormBounds = (roots, source) => {
28
28
  if (schemas.length === 0 || operationRoots.length === 0) {
29
29
  process.stdout.write("codegen: no GraphQL contract under be/contracts yet\n")
30
30
  if (schemas.length === 0 && operationRoots.length) {
31
- const { emptyInputBoundsSource } = await import("@starci/cli/input-bounds")
31
+ const { emptyInputBoundsSource } = await import("starci-harness/input-bounds")
32
32
  writeFormBounds(operationRoots, emptyInputBoundsSource())
33
33
  }
34
34
  } else {
35
35
  const { buildSchema } = await import("graphql")
36
- const { assertCompatibleInputBounds } = await import("@starci/cli/input-bounds")
36
+ const { assertCompatibleInputBounds } = await import("starci-harness/input-bounds")
37
37
  assertCompatibleInputBounds(
38
38
  Object.fromEntries(schemas.map((file) => [file, buildSchema(fs.readFileSync(file, "utf8"))])),
39
39
  )
@@ -49,7 +49,7 @@ if (schemas.length === 0 || operationRoots.length === 0) {
49
49
  config: { useTypeImports: true, enumsAsTypes: true, avoidOptionals: true },
50
50
  },
51
51
  ],
52
- [`${root}/__generated__/input-bounds.ts`, { plugins: [{ "@starci/cli/input-bounds": {} }] }],
52
+ [`${root}/__generated__/input-bounds.ts`, { plugins: [{ "starci-harness/input-bounds": {} }] }],
53
53
  ]),
54
54
  )
55
55
  await generate(
@@ -80,7 +80,7 @@ const restRoots = frontendApps
80
80
  .filter((dir) => fs.existsSync(dir))
81
81
  .map(posix)
82
82
  if (restRoots.length) {
83
- const { openApiBoundsSource } = await import("@starci/cli/input-bounds")
83
+ const { openApiBoundsSource } = await import("starci-harness/input-bounds")
84
84
  const contracts = Object.fromEntries(
85
85
  services
86
86
  .map((service) => [service, path.join(CONTRACTS, service, "openapi.json")])
@@ -1,11 +1,13 @@
1
1
  {{> app/quality-config/sonar-base.properties}}
2
2
  {{sonarHostLine}}
3
- sonar.tests=be/apps,be/src
3
+ sonar.tests={{sonarSources}}
4
4
  sonar.exclusions={{sonarExclusions}}
5
5
  sonar.cpd.exclusions={{cpdExclusions}}
6
- sonar.test.inclusions=**/*.spec.ts
6
+ sonar.test.inclusions={{sonarTestInclusions}}
7
7
  sonar.typescript.tsconfigPaths={{tsconfigPaths}}
8
8
  sonar.externalIssuesReportPaths=reports/lint.sonar.json
9
9
  sonar.javascript.lcov.reportPaths={{lcovReport}}
10
+ # starci:sonar-scope begin (generated by `starci app sonar --write` from contexts/hfs/slots.yaml; do not edit)
10
11
  sonar.coverage.exclusions={{coverageExclusions}}
12
+ # starci:sonar-scope end
11
13
  sonar.javascript.node.maxspace=6144
@@ -1,14 +0,0 @@
1
- // canonical-json.mjs — the one canonical JSON the runtime digests structured values with: object keys
2
- // sorted, arrays in order, so two equal values always serialize to the same bytes. A leaf beside digest.mjs.
3
- import {byCodeUnit} from './by-code-unit.mjs';
4
- import {isPlainObject} from './plain-object.mjs';
5
-
6
- /** The canonical JSON text of `value`: keys sorted at every depth. */
7
- export function canonicalJSON(value) {
8
- if (Array.isArray(value)) return `[${value.map(canonicalJSON).join(',')}]`;
9
- if (isPlainObject(value)) {
10
- const members = Object.keys(value).sort(byCodeUnit).map(k => `${JSON.stringify(k)}:${canonicalJSON(value[k])}`);
11
- return `{${members.join(',')}}`;
12
- }
13
- return JSON.stringify(value);
14
- }
@@ -1,38 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { createRequire } from 'node:module';
4
- import { installRefResolver } from './ref-value.mjs';
5
-
6
- // engine/db/ledger-open.mjs — how a ledger file is opened: bounded retry on a transient CANTOPEN, a reference-resolving handle.
7
- const require=createRequire(import.meta.url);
8
- export const need=(ok,message,code)=>{if(!ok)throw Object.assign(new Error(message),code?{code}:{});};
9
- export const applyPragmas=(db,pragmas)=>db.exec(Object.entries(pragmas).map(([k,v])=>`PRAGMA ${k}=${v};`).join(' '));
10
- const openSleep=ms=>Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,ms);
11
- // SQLITE_CANTOPEN is transient on Windows while a concurrent process closes the WAL files: a short bounded retry.
12
- const OPEN_RETRY_DELAYS_MS=[0,300,900];
13
- const cantOpen=error=>/unable to open/i.test(String(error?.message??''));
14
- export function openDb({file,busyTimeoutMs,journalMode='WAL',autoVacuum=false,label,pragmas}){
15
- const {DatabaseSync}=require('node:sqlite');
16
- need(typeof file==='string'&&file.trim(),`${label} needs a file`);
17
- fs.mkdirSync(path.dirname(path.resolve(file)),{recursive:true});
18
- let lastError;
19
- for(const delay of OPEN_RETRY_DELAYS_MS){
20
- if(delay)openSleep(delay);
21
- let db;
22
- try{
23
- db=installRefResolver(new DatabaseSync(file,{timeout:busyTimeoutMs}));
24
- // page_size/auto_vacuum only take on an empty database, before WAL and before the first table.
25
- if(autoVacuum&&Number(db.prepare('PRAGMA page_count').get().page_count)===0)db.exec('PRAGMA page_size=4096; PRAGMA auto_vacuum=INCREMENTAL;');
26
- const sqliteVersion=db.prepare('select sqlite_version() AS version').get().version;
27
- const actual=String(db.prepare(`PRAGMA journal_mode=${journalMode}`).get().journal_mode).toUpperCase();
28
- need(actual===String(journalMode).toUpperCase(),`${label}: SQLite selected journal mode ${actual}, expected ${journalMode} (a network or UNC path cannot hold WAL)`,'STARCI_LEDGER_NOT_WAL');
29
- applyPragmas(db,pragmas);
30
- return {db,sqliteVersion,journalMode:actual};
31
- }catch(error){
32
- try{db?.close();}catch{}
33
- if(!cantOpen(error))throw error;
34
- lastError=error;
35
- }
36
- }
37
- throw lastError;
38
- }
@@ -1,87 +0,0 @@
1
- // Ledger path identity and ownership resolution; connection policy and caches remain in ledger.mjs.
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import {sha256} from '../digest.mjs';
5
- import {isUnderTempDir,localProjectsRoot,readMachine} from './machine.mjs';
6
- import {isSpecRun} from '../../scripts/lib/env.mjs';
7
- import {pathKey} from '../../scripts/lib/path-key.mjs';
8
- import {byCodeUnit} from '../by-code-unit.mjs';
9
- import {tempRoot} from '../temp-root.mjs';
10
- /** Overrides the projects root (the directory holding <ledger_id>/runtime.sqlite) for this process tree; narrower than machine-db.mjs LOCAL_ROOT_ENV, which this still honors through starciLocalRoot when unset. */
11
- export const PROJECTS_ROOT_ENV='STARCI_PROJECTS_ROOT';
12
- const normDir=file=>pathKey(file);
13
- /** <runtime root>/.runtime/projects (starciLocalRoot, itself overridable by STARCI_LOCAL_ROOT; a node --test process tree gets one under the OS temp directory). */
14
- export const projectsRootFor=(env=process.env)=>{
15
- if(env[PROJECTS_ROOT_ENV])return path.resolve(env[PROJECTS_ROOT_ENV]);
16
- const root=localProjectsRoot(env);
17
- if(isSpecRun(env)&&!isUnderTempDir(root,{env}))return path.join(tempRoot({env}),'starci-test-projects');
18
- return root;
19
- };
20
- /** Repository identity: resolved/realpath, forward slashes, case-folded only on Windows. */
21
- export const repoRootKey=repoRoot=>{let root=path.resolve(repoRoot);try{root=fs.realpathSync.native(root);}catch{}return normDir(root);};
22
- /**
23
- * The ledger id of a repository root until the machine registry (a3-2 machine.ledgers) resolves it: a name-based
24
- * UUID of the normalized root, so the same root always names the same ledger and no side registry is needed.
25
- */
26
- const ledgerIdFromKey=key=>{
27
- const h=sha256(`starci-ledger:${key}`);
28
- return `${h.slice(0,8)}-${h.slice(8,12)}-5${h.slice(13,16)}-${(8|(Number.parseInt(h[16],16)&3)).toString(16)}${h.slice(17,20)}-${h.slice(20,32)}`;
29
- };
30
- export const ledgerIdForRepo=repoRoot=>ledgerIdFromKey(repoRootKey(repoRoot));
31
- /** The registered runtime.sqlite of `repoRoot` in machine.ledgers (a3-2 resolveLedger), or null. */
32
- function registeredLedgerFile(root,env,openReader){
33
- let row;try{row=readMachine(m=>m.resolveLedger({repoRoot:root}),null,{env});}catch{return null;}
34
- const file=row?.file&&row.state!=='retired'?path.resolve(row.file):null;
35
- if(file&&fs.existsSync(file))assertLedgerRoot(file,root,openReader);
36
- return file;
37
- }
38
- function assertLedgerRoot(file,root,openReader){
39
- const db=openReader(file);let own;try{own=db.prepare("SELECT value FROM meta WHERE key='repo_root'").get()?.value;}finally{db.close();}
40
- if(!own||repoRootKey(own)!==repoRootKey(root))throw Object.assign(new Error(`ledger-root-mismatch: ${file} belongs to another or unverified repository; preserve the database and resolve the owner binding with the owner`),{code:'STARCI_LEDGER_ROOT_MISMATCH'});
41
- }
42
- function unregisteredLedgerFile(root,env,openReader){
43
- const base=projectsRootFor(env),current=path.join(base,ledgerIdForRepo(root),'runtime.sqlite');
44
- if(fs.existsSync(current)){assertLedgerRoot(current,root,openReader);return current;}
45
- return current;
46
- }
47
- /** Resolve the registry or exact owned store; the ledger owner supplies its verified read-only opener. */
48
- export function resolveLedgerFile(root,{env,openReader}){
49
- return registeredLedgerFile(root,env,openReader)??unregisteredLedgerFile(root,env,openReader);
50
- }
51
-
52
- const SYNTHETIC_LEDGER_ID=/^00000000-0000-4000-8000-[0-9a-f]{12}$/;
53
- const fixtureRefused=message=>{throw new TypeError(`ledger-fixture-refused: ${message}`);};
54
- const assertFixtureIdentity=fixture=>{
55
- if(fixture.sqliteVersion!==undefined&&(typeof fixture.sqliteVersion!=='string'||!/^\d+\.\d+\.\d+(?:\.\d+)?$/.test(fixture.sqliteVersion)))fixtureRefused('sqliteVersion must be a dotted numeric version');
56
- if(typeof fixture.ledgerId!=='string'||!SYNTHETIC_LEDGER_ID.test(fixture.ledgerId))fixtureRefused('ledgerId must be a synthetic UUID');
57
- if(!Number.isSafeInteger(fixture.createdAt)||fixture.createdAt<=0||!Number.isFinite(new Date(fixture.createdAt).getTime()))fixtureRefused('createdAt must be a positive safe epoch millisecond');
58
- const root=fixture.blobRoot;
59
- if(typeof root!=='string'||!root||root.includes('\\')||root.trim()!==root||/[<>:"|?*\x00-\x1f]/.test(root)
60
- ||path.posix.isAbsolute(root)||root.split('/').some(p=>!p||p==='.'||p==='..'))fixtureRefused('blobRoot must be a portable relative path');
61
- };
62
- const assertFixtureFile=file=>{
63
- if(typeof file!=='string'||!path.isAbsolute(file)||path.basename(file)!=='runtime.sqlite')fixtureRefused('file must be an explicit absolute runtime.sqlite');
64
- for(const suffix of ['','-wal','-shm'])if(fs.lstatSync(file+suffix,{throwIfNoEntry:false}))fixtureRefused('file and WAL/SHM must be absent');
65
- const parent=path.dirname(file),stat=fs.lstatSync(parent,{throwIfNoEntry:false});
66
- if(!stat?.isDirectory()||stat.isSymbolicLink()||fs.realpathSync.native(parent)!==path.resolve(parent))fixtureRefused('sample parent must be an existing physical directory');
67
- };
68
- /** Validate a fresh, initialization-only sample identity. Samples carry portable metadata,
69
- * never a repository/machine binding; the ordinary project resolver is not bypassed. */
70
- export function ledgerFixtureInit(fixture,{file,repoRoot,product,machine,mapped,checkpointer,now}){
71
- if(fixture===null){
72
- if(typeof file==='string'&&SYNTHETIC_LEDGER_ID.test(path.basename(path.dirname(path.resolve(file)))))fixtureRefused('synthetic identity is reserved for a sample');
73
- return null;
74
- }
75
- if(!fixture||typeof fixture!=='object'||Array.isArray(fixture)
76
- ||![Object.prototype,null].includes(Object.getPrototypeOf(fixture))
77
- ||!['blobRoot,createdAt,ledgerId','blobRoot,createdAt,ledgerId,sqliteVersion'].includes(Object.keys(fixture).sort(byCodeUnit).join(',')))fixtureRefused('needs ledgerId, createdAt and blobRoot, and may state sqliteVersion');
78
- assertFixtureIdentity(fixture);
79
- if(repoRoot!==null||product!==null||machine!==null||mapped)fixtureRefused('a sample cannot bind a repository, product, machine or cached project path');
80
- if(!checkpointer||now!==Date.now)fixtureRefused('initialization owns its frozen clock and requires a checkpointer');
81
- assertFixtureFile(file);
82
- return Object.freeze({...fixture,marker:'starci/basic-runtime-fixture@1'});
83
- }
84
- /** Refuse sample bytes as an operational ledger before opening a writer or beginning a transaction. */
85
- export function assertOperationalLedger(meta){
86
- if(meta.fixture||SYNTHETIC_LEDGER_ID.test(meta.ledger_id??''))throw new Error('ledger-fixture-read-only: preserve the sample; initialize a new live project store');
87
- }