@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.
- package/CHANGELOG.md +18 -0
- package/README.md +4 -9
- package/emit/operation-schemas.mjs +3 -0
- package/lint/bound-sys.cjs +2 -2
- package/package.json +11 -5
- package/runtime/engine/admission.mjs +1 -1
- package/runtime/engine/config.mjs +2 -2
- package/runtime/engine/db/machine.mjs +2 -3
- package/runtime/engine/invalid-config.mjs +1 -1
- package/runtime/knowledge/grammars/{starci → core}/playbook.yaml +3 -3
- package/runtime/knowledge/grammars/index.yaml +14 -23
- package/runtime/knowledge/grammars/offset-pop/playbook.yaml +2 -2
- package/runtime/knowledge/hfs/canon-pins.yaml +23 -23
- package/runtime/knowledge/hfs/fe-stack.yaml +1 -1
- package/runtime/knowledge/hfs/rules.yaml +83 -60
- package/runtime/modules/kernel/failure-codes.yaml +23 -13
- package/runtime/scripts/guards/git-policy.mjs +2 -3
- package/runtime/scripts/hfs/rule-catalog.mjs +2 -2
- package/runtime/scripts/hfs/rules/pipeline.mjs +5 -5
- package/runtime/scripts/lib/example-refs.mjs +10 -3
- package/runtime/scripts/lib/grammar-lint.mjs +16 -3
- package/runtime/scripts/lib/package-at.mjs +5 -6
- package/runtime/scripts/lib/roots.mjs +2 -2
- package/runtime/scripts/work/draw/draw-dna-findings.mjs +23 -12
- package/runtime/scripts/work/draw/draw-dna.mjs +14 -8
- package/scaffold/app.mjs +6 -6
- package/src/main.mjs +3 -3
- package/sync/hygiene.mjs +1 -1
- package/sync/index.mjs +15 -2
- package/sync/managed.mjs +3 -2
- package/templates/app/codegen-graphql.mjs.partial +4 -4
- package/templates/app/quality-config/sonar-project.properties +4 -2
- package/runtime/engine/canonical-json.mjs +0 -14
- package/runtime/engine/db/ledger-open.mjs +0 -38
- package/runtime/engine/db/ledger-paths.mjs +0 -87
- package/runtime/engine/db/ledger.mjs +0 -1068
- package/runtime/engine/db/provision-authority.mjs +0 -76
- package/runtime/engine/db/provision-operations.mjs +0 -146
- package/runtime/packages/grammar/scripts/build-stamp.mjs +0 -98
- package/runtime/scripts/agent/codex-app-server.mjs +0 -79
- package/runtime/scripts/agent/codex-mirror-source.mjs +0 -34
- package/runtime/scripts/agent/host-agents.mjs +0 -80
- package/runtime/scripts/agent/launch-trust-policy.mjs +0 -53
- package/runtime/scripts/agent/trust-launch.mjs +0 -191
- package/runtime/scripts/agent/trust.mjs +0 -523
- package/runtime/scripts/api/fs/temp-path.mjs +0 -8
- package/runtime/scripts/api/git/diff-names.mjs +0 -8
- package/runtime/scripts/api/git/head-time.mjs +0 -10
- package/runtime/scripts/api/git/index-lock.mjs +0 -78
- package/runtime/scripts/api/git/log.mjs +0 -5
- package/runtime/scripts/api/git/merge-tree.mjs +0 -5
- package/runtime/scripts/api/git/rev-list.mjs +0 -5
- package/runtime/scripts/api/git/rev-parse.mjs +0 -13
- package/runtime/scripts/api/git/status-query.mjs +0 -5
- package/runtime/scripts/api/git/worktree-list-query.mjs +0 -5
- package/runtime/scripts/api/node/spawn-node.mjs +0 -6
- package/runtime/scripts/api/npm/dist-tag.mjs +0 -8
- package/runtime/scripts/api/npm/lib.mjs +0 -32
- package/runtime/scripts/api/npm/pack-dry-run.mjs +0 -9
- package/runtime/scripts/api/npm/run-npm.mjs +0 -6
- package/runtime/scripts/api/npm/view.mjs +0 -30
- package/runtime/scripts/api/npm/whoami.mjs +0 -8
- package/runtime/scripts/api/process/hide-child-windows.mjs +0 -31
- package/runtime/scripts/api/process/kill-tree.mjs +0 -22
- package/runtime/scripts/api/process/lib.mjs +0 -125
- package/runtime/scripts/api/process/probe-guard-command.mjs +0 -74
- package/runtime/scripts/api/process/process-list.mjs +0 -28
- package/runtime/scripts/api/process/system-tool.mjs +0 -37
- package/runtime/scripts/gates/canon-digest.mjs +0 -81
- package/runtime/scripts/gates/canon-pins.mjs +0 -119
- package/runtime/scripts/gates/gate-input.mjs +0 -98
- package/runtime/scripts/gates/gate.mjs +0 -636
- package/runtime/scripts/gates/grammar-dist.mjs +0 -206
- package/runtime/scripts/gates/output.mjs +0 -40
- package/runtime/scripts/gates/runtime-package-selection.mjs +0 -27
- package/runtime/scripts/gates/test-world-run.mjs +0 -252
- package/runtime/scripts/gates/type-impact.mjs +0 -277
- package/runtime/scripts/gates/type-program.mjs +0 -113
- package/runtime/scripts/guards/assigned-command.mjs +0 -12
- package/runtime/scripts/guards/call-rights.mjs +0 -47
- package/runtime/scripts/guards/command-guard.mjs +0 -468
- package/runtime/scripts/guards/command-policy.mjs +0 -342
- package/runtime/scripts/guards/critic-reach.mjs +0 -82
- package/runtime/scripts/guards/deps-guard.mjs +0 -150
- package/runtime/scripts/guards/env-dump-verdict.mjs +0 -89
- package/runtime/scripts/guards/guards-root.mjs +0 -27
- package/runtime/scripts/guards/hook-install.mjs +0 -343
- package/runtime/scripts/guards/hook-verdict.mjs +0 -12
- package/runtime/scripts/guards/install-verdict.mjs +0 -41
- package/runtime/scripts/guards/kernel-seat.mjs +0 -63
- package/runtime/scripts/guards/launch-verdict.mjs +0 -74
- package/runtime/scripts/guards/op-starci.mjs +0 -16
- package/runtime/scripts/guards/op-write-scope.mjs +0 -27
- package/runtime/scripts/guards/orca-self-lifecycle.mjs +0 -39
- package/runtime/scripts/guards/process-kill-verdict.mjs +0 -106
- package/runtime/scripts/guards/protected-zone.mjs +0 -56
- package/runtime/scripts/guards/refusals.mjs +0 -27
- package/runtime/scripts/guards/rights.mjs +0 -268
- package/runtime/scripts/guards/seat-tools.mjs +0 -47
- package/runtime/scripts/guards/shell-commands.mjs +0 -328
- package/runtime/scripts/guards/supervisor-seat.mjs +0 -80
- package/runtime/scripts/guards/verdict-process.mjs +0 -81
- package/runtime/scripts/lib/app-data.mjs +0 -11
- package/runtime/scripts/lib/canon-binding-text.mjs +0 -129
- package/runtime/scripts/lib/check-format.mjs +0 -7
- package/runtime/scripts/lib/failure-code-reads.mjs +0 -100
- package/runtime/scripts/lib/failure-code-template-errors.mjs +0 -161
- package/runtime/scripts/lib/git-dir.mjs +0 -120
- package/runtime/scripts/lib/guard-command.mjs +0 -24
- package/runtime/scripts/lib/interrupt-cleanup.mjs +0 -38
- package/runtime/scripts/lib/json-formats.mjs +0 -34
- package/runtime/scripts/lib/managed-json.mjs +0 -51
- package/runtime/scripts/lib/menu-parts.mjs +0 -11
- package/runtime/scripts/lib/op-gate.mjs +0 -15
- package/runtime/scripts/lib/provision-control-failure.mjs +0 -32
- package/runtime/scripts/lib/release-version.mjs +0 -134
- package/runtime/scripts/lib/runtime-location.mjs +0 -21
- package/runtime/scripts/lib/runtime-package-inventory.mjs +0 -21
- package/runtime/scripts/lib/runtime-package-manifests.mjs +0 -84
- package/runtime/scripts/lib/runtime-publication-tags.mjs +0 -56
- package/runtime/scripts/lib/seat-env.mjs +0 -43
- package/runtime/scripts/lib/sonar-new-code.mjs +0 -58
- package/runtime/scripts/lib/source-package-imports.mjs +0 -111
- package/runtime/scripts/lib/tar-files.mjs +0 -39
- package/runtime/scripts/lib/uat-run-folder.mjs +0 -118
- package/runtime/scripts/lib/worktree-record.mjs +0 -14
- package/runtime/scripts/machine/ask-receipts.mjs +0 -113
- package/runtime/scripts/machine/evidence-store.mjs +0 -223
- package/runtime/scripts/machine/home.mjs +0 -216
- package/runtime/scripts/machine/host-lock.mjs +0 -184
- package/runtime/scripts/machine/lock-recovery.mjs +0 -55
- package/runtime/scripts/machine/roles-contract.mjs +0 -90
- package/runtime/scripts/machine/runtime-change.mjs +0 -20
- package/runtime/scripts/machine/workflow-tree.mjs +0 -122
- package/runtime/scripts/reconciler/checklist-items.mjs +0 -7
- package/runtime/scripts/reconciler/guard-row.mjs +0 -25
- package/runtime/scripts/work/brand/brand-colour.mjs +0 -125
- package/runtime/scripts/work/brand/brand-tokens.mjs +0 -167
- package/runtime/scripts/work/brand/brand-value-source.mjs +0 -106
- package/runtime/scripts/work/brand/brand.mjs +0 -892
- package/runtime/scripts/work/draw/draw-layer.mjs +0 -306
- package/runtime/scripts/work/draw/draw-rationale-page.mjs +0 -237
- package/runtime/scripts/work/draw/draw-rationale-ref.mjs +0 -14
- package/runtime/scripts/work/draw/draw-rationale.mjs +0 -353
- package/runtime/scripts/work/draw/draw-source.mjs +0 -412
- package/runtime/scripts/work/draw/draw-taste.mjs +0 -278
- package/runtime/scripts/work/draw/draw-typecheck.mjs +0 -72
- package/runtime/scripts/work/draw/rationale-findings.mjs +0 -205
- package/runtime/scripts/work/draw-grammar.mjs +0 -175
- package/runtime/scripts/work/draw-render-page.mjs +0 -188
- package/runtime/scripts/work/draw-render.mjs +0 -626
- package/runtime/scripts/work/png.mjs +0 -237
- 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
|
|
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, '
|
|
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
|
|
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
|
|
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, '
|
|
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:
|
|
15
|
-
//
|
|
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
|
-
|
|
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
|
|
225
|
-
for (const el of all)
|
|
224
|
+
function statusDotFindings(all, rules, add, h) {
|
|
225
|
+
for (const el of all) statusDotFindingFor(el, rules, add, h);
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
|
|
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
|
|
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 = [
|
|
235
|
-
|
|
236
|
-
|
|
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, '
|
|
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.
|
|
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
|
|
21
|
-
//
|
|
22
|
-
// a hand-made dot span, a dot with a
|
|
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.
|
|
71
|
-
const
|
|
72
|
-
const
|
|
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,
|
|
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: '
|
|
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: '
|
|
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/
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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:
|
|
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,
|
|
27
|
-
//
|
|
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("
|
|
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("
|
|
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: [{ "
|
|
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("
|
|
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=
|
|
3
|
+
sonar.tests={{sonarSources}}
|
|
4
4
|
sonar.exclusions={{sonarExclusions}}
|
|
5
5
|
sonar.cpd.exclusions={{cpdExclusions}}
|
|
6
|
-
sonar.test.inclusions
|
|
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
|
-
}
|