@starci/hfs 4.0.10 → 4.1.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 +1 -1
- package/emit/operations.mjs +1 -1
- package/emit/type-schema.mjs +2 -1
- package/lint/run.mjs +3 -2
- package/package.json +1 -1
- package/report/order.mjs +2 -0
- package/runtime/config.example.yaml +187 -0
- package/runtime/engine/by-code-unit.mjs +12 -0
- package/runtime/engine/config.mjs +221 -239
- package/runtime/engine/invalid-config.mjs +5 -5
- package/runtime/engine/model-config.mjs +90 -0
- package/runtime/engine/orca-config.mjs +3 -1
- package/runtime/engine/removed-vocabulary.mjs +28 -0
- package/runtime/engine/resources-config.mjs +52 -0
- package/runtime/engine/runtime-root.mjs +17 -0
- package/runtime/engine/secrets.mjs +35 -31
- package/runtime/engine/sonar-config.mjs +21 -0
- package/runtime/engine/temp-root.mjs +32 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
- package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
- package/runtime/knowledge/hfs/rules.yaml +98 -65
- package/runtime/knowledge/hfs/slots.yaml +11 -14
- package/runtime/knowledge/patterns/be/api.yaml +7 -24
- package/runtime/knowledge/patterns/be/cli.yaml +3 -12
- package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
- package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
- package/runtime/modules/kernel/failure-codes.yaml +1 -41
- package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
- package/runtime/modules/models/registry.yaml +25 -359
- package/runtime/modules/models/runtimes.yaml +92 -256
- package/runtime/modules/models/tiers.yaml +65 -0
- package/runtime/scripts/api/fs/claim-file.mjs +30 -0
- package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
- package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
- package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
- package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
- package/runtime/scripts/api/git/lib.mjs +34 -1
- package/runtime/scripts/api/process/resolve-real-tool.mjs +14 -8
- package/runtime/scripts/api/process/run-program.mjs +2 -1
- package/runtime/scripts/api/sops/lib.mjs +198 -120
- package/runtime/scripts/hfs/allows.mjs +3 -3
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
- package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
- package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
- package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
- package/runtime/scripts/hfs/architecture/config.mjs +95 -80
- package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
- package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
- package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
- package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
- package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
- package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
- package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
- package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
- package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
- package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
- package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
- package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
- package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
- package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
- package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
- package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
- package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
- package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
- package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
- package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
- package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
- package/runtime/scripts/hfs/architecture/index.mjs +158 -102
- package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
- package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
- package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
- package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
- package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
- package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
- package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
- package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
- package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
- package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
- package/runtime/scripts/hfs/architecture/required-files.mjs +100 -68
- package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
- package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
- package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
- package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
- package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
- package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
- package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
- package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
- package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
- package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
- package/runtime/scripts/hfs/architecture.mjs +3 -4
- package/runtime/scripts/hfs/check.mjs +61 -70
- package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
- package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
- package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
- package/runtime/scripts/hfs/edition-slots.mjs +5 -3
- package/runtime/scripts/hfs/linear-text.mjs +30 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
- package/runtime/scripts/hfs/path-findings.mjs +58 -38
- package/runtime/scripts/hfs/pin-findings.mjs +31 -0
- package/runtime/scripts/hfs/repo-identity.mjs +5 -2
- package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
- package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
- package/runtime/scripts/hfs/rules/cli.mjs +5 -1
- package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
- package/runtime/scripts/hfs/rules/contract.mjs +35 -27
- package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
- package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
- package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
- package/runtime/scripts/hfs/rules/database.mjs +36 -23
- package/runtime/scripts/hfs/rules/deps.mjs +64 -32
- package/runtime/scripts/hfs/rules/docker.mjs +113 -65
- package/runtime/scripts/hfs/rules/edition.mjs +118 -111
- package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
- package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
- package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
- package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
- package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
- package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
- package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
- package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
- package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
- package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
- package/runtime/scripts/hfs/rules/saga.mjs +128 -61
- package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
- package/runtime/scripts/hfs/rules/services.mjs +34 -16
- package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
- package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
- package/runtime/scripts/hfs/secret.mjs +59 -48
- package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
- package/runtime/scripts/hfs/slot-classify.mjs +65 -0
- package/runtime/scripts/hfs/slot-errors.mjs +11 -0
- package/runtime/scripts/hfs/slot-imports.mjs +76 -0
- package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
- package/runtime/scripts/hfs/slot-match.mjs +153 -0
- package/runtime/scripts/hfs/slot-path.mjs +8 -0
- package/runtime/scripts/hfs/slot-required.mjs +76 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
- package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
- package/runtime/scripts/hfs/slots.mjs +72 -693
- package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
- package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
- package/runtime/scripts/hfs/tree.mjs +19 -14
- package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
- package/runtime/scripts/hfs/view.mjs +1 -1
- package/runtime/scripts/lib/dockerfile.mjs +56 -33
- package/runtime/scripts/lib/event-contract.mjs +17 -13
- package/runtime/scripts/lib/git.mjs +2 -2
- package/runtime/scripts/lib/graphql-contract.mjs +158 -322
- package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
- package/runtime/scripts/lib/i18n.mjs +3 -2
- package/runtime/scripts/lib/in-order.mjs +71 -0
- package/runtime/scripts/lib/language.mjs +3 -3
- package/runtime/scripts/lib/list.mjs +8 -1
- package/runtime/scripts/lib/path-key.mjs +38 -5
- package/runtime/scripts/lib/pid-alive.mjs +7 -0
- package/runtime/scripts/lib/regex.mjs +1 -1
- package/runtime/scripts/lib/same-text.mjs +1 -1
- package/runtime/scripts/lib/secret-patterns.mjs +6 -2
- package/runtime/scripts/lib/sleep-sync.mjs +2 -2
- package/runtime/scripts/lib/sops-envelope.mjs +95 -48
- package/runtime/scripts/lib/stack-services.mjs +1 -2
- package/runtime/scripts/lib/ts-ast.mjs +9 -0
- package/runtime/scripts/lib/walk.mjs +6 -1
- package/scaffold/add-cli-lite.mjs +2 -1
- package/scaffold/add-table.mjs +2 -2
- package/scaffold/add.mjs +3 -2
- package/scaffold/app.mjs +4 -2
- package/scaffold/edition-gate.mjs +25 -25
- package/scaffold/lite-exports.mjs +2 -1
- package/scaffold/service.mjs +5 -4
- package/sync/hygiene.mjs +15 -9
- package/sync/index.mjs +7 -3
- package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
- package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
- package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
- package/templates/app/ci-workflows/sonar-steps.yml +2 -2
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
- package/templates/app/starciwork.gitignore +1 -1
- package/templates/be/image/api/Dockerfile +1 -1
- package/templates/be/image/cli/Dockerfile +1 -1
- package/templates/be/image/worker/Dockerfile +1 -1
- package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
- package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
- package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
- package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
- package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
- package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
- package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
- package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
- package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
- package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
- package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
- package/templates/fe/image/next/Dockerfile +2 -1
- package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
- package/upgrade/index.mjs +9 -2
- package/runtime/engine/admission.mjs +0 -308
- package/runtime/engine/canonical-json.mjs +0 -10
- package/runtime/engine/db/blob.mjs +0 -315
- package/runtime/engine/db/ledger-paths.mjs +0 -83
- package/runtime/engine/db/ledger.mjs +0 -1118
- package/runtime/engine/db/machine-connection.mjs +0 -135
- package/runtime/engine/db/machine-schema.mjs +0 -84
- package/runtime/engine/db/machine.mjs +0 -1367
- package/runtime/engine/db/migrations/machine/0001-init.sql +0 -924
- package/runtime/engine/db/migrations/runtime/0001-init.sql +0 -1108
- package/runtime/engine/db/provider-reservations.mjs +0 -101
- package/runtime/engine/digest.mjs +0 -16
- package/runtime/engine/refuse.mjs +0 -11
- package/runtime/modules/ops/_labels.yaml +0 -53
- package/runtime/scripts/api/node/lib.mjs +0 -14
- package/runtime/scripts/api/node/spawn-node.mjs +0 -6
- package/runtime/scripts/api/process/lib.mjs +0 -111
- package/runtime/scripts/api/process/owned-process.mjs +0 -78
- package/runtime/scripts/api/process/stop-owned-process.mjs +0 -9
- package/runtime/scripts/connectors/lib.mjs +0 -488
- package/runtime/scripts/lib/clip.mjs +0 -19
- package/runtime/scripts/lib/display-names.mjs +0 -259
- package/runtime/scripts/lib/example-refs.mjs +0 -158
- package/runtime/scripts/lib/json-schema.mjs +0 -52
- package/runtime/scripts/lib/process-identity.mjs +0 -6
- package/runtime/scripts/lib/read-yaml.mjs +0 -18
- package/runtime/scripts/lib/redact.mjs +0 -161
- package/runtime/scripts/lib/sleep.mjs +0 -7
- package/runtime/scripts/lib/source-phrases.mjs +0 -34
- package/runtime/scripts/lib/sqlite.mjs +0 -21
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// graphql-sdl.mjs - the GraphQL tokenizer and the schema (SDL) reader of graphql-contract.mjs: tokens, a cursor over them, the
|
|
2
|
+
// type, value and directive readers a document shares, and `parseSchema` over the SDL `starci app emit` writes. No dependency:
|
|
3
|
+
// the runtime ships no GraphQL library.
|
|
4
|
+
|
|
5
|
+
const PUNCTUATORS = new Set(['{', '}', '(', ')', '[', ']', ':', '!', '=', '$', '@', '|', '&']);
|
|
6
|
+
|
|
7
|
+
/** The tokens of a GraphQL source: names, punctuators, `...`, strings and numbers; comments and commas are insignificant. */
|
|
8
|
+
function commentEnd(source, index) { while (index < source.length && source[index] !== '\n') { index += 1; } return index; }
|
|
9
|
+
function blockStringAt(source, index) { const end = source.indexOf('"""', index + 3); if (end === -1) { throw new SyntaxError('unterminated block string'); } return { token: { kind: 'string', value: source.slice(index + 3, end) }, next: end + 3 }; }
|
|
10
|
+
function quotedStringAt(source, index) { let end = index + 1; while (end < source.length && source[end] !== '"') { end += source[end] === '\\' ? 2 : 1; } return { token: { kind: 'string', value: source.slice(index + 1, end) }, next: end + 1 }; }
|
|
11
|
+
function nameAt(source, index) { const match = /^[_A-Za-z]\w*/.exec(source.slice(index)); return { token: { kind: 'name', value: match[0] }, next: index + match[0].length }; }
|
|
12
|
+
function numberAt(source, index) { return /^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/.exec(source.slice(index)); }
|
|
13
|
+
export function tokenize(source) {
|
|
14
|
+
const tokens = [];
|
|
15
|
+
let index = 0;
|
|
16
|
+
while (index < source.length) {
|
|
17
|
+
const char = source[index];
|
|
18
|
+
if (char === '#') {
|
|
19
|
+
index = commentEnd(source, index);
|
|
20
|
+
} else if (/[\s,]/.test(char)) {
|
|
21
|
+
index += 1;
|
|
22
|
+
} else if (source.startsWith('...', index)) {
|
|
23
|
+
tokens.push({ kind: 'spread', value: '...' });
|
|
24
|
+
index += 3;
|
|
25
|
+
} else if (source.startsWith('"""', index)) {
|
|
26
|
+
const parsed = blockStringAt(source, index);
|
|
27
|
+
tokens.push(parsed.token);
|
|
28
|
+
index = parsed.next;
|
|
29
|
+
} else if (char === '"') {
|
|
30
|
+
const parsed = quotedStringAt(source, index);
|
|
31
|
+
tokens.push(parsed.token);
|
|
32
|
+
index = parsed.next;
|
|
33
|
+
} else if (PUNCTUATORS.has(char)) {
|
|
34
|
+
tokens.push({ kind: 'punct', value: char });
|
|
35
|
+
index += 1;
|
|
36
|
+
} else if (/[_A-Za-z]/.test(char)) {
|
|
37
|
+
const parsed = nameAt(source, index);
|
|
38
|
+
tokens.push(parsed.token);
|
|
39
|
+
index = parsed.next;
|
|
40
|
+
} else if (/[-0-9]/.test(char)) {
|
|
41
|
+
const match = numberAt(source, index);
|
|
42
|
+
if (!match) throw new SyntaxError(`unexpected character "${char}"`);
|
|
43
|
+
tokens.push({ kind: 'number', value: match[0] });
|
|
44
|
+
index += match[0].length;
|
|
45
|
+
} else {
|
|
46
|
+
throw new SyntaxError(`unexpected character "${char}"`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return tokens;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A cursor over tokens with the expectations both parsers share. */
|
|
53
|
+
export function cursorOf(tokens) {
|
|
54
|
+
let index = 0;
|
|
55
|
+
const cursor = {
|
|
56
|
+
peek: (offset = 0) => tokens[index + offset] ?? null,
|
|
57
|
+
next: () => {
|
|
58
|
+
const token = tokens[index];
|
|
59
|
+
if (token === undefined) throw new SyntaxError('unexpected end of document');
|
|
60
|
+
index += 1;
|
|
61
|
+
return token;
|
|
62
|
+
},
|
|
63
|
+
is: (value) => tokens[index]?.value === value && tokens[index]?.kind !== 'string',
|
|
64
|
+
done: () => index >= tokens.length,
|
|
65
|
+
take: (value) => {
|
|
66
|
+
if (!cursor.is(value)) return false;
|
|
67
|
+
index += 1;
|
|
68
|
+
return true;
|
|
69
|
+
},
|
|
70
|
+
expect: (value) => {
|
|
71
|
+
const token = cursor.next();
|
|
72
|
+
if (token.value !== value || token.kind === 'string') throw new SyntaxError(`expected "${value}", found "${token.value}"`);
|
|
73
|
+
return token;
|
|
74
|
+
},
|
|
75
|
+
name: () => {
|
|
76
|
+
const token = cursor.next();
|
|
77
|
+
if (token.kind !== 'name') throw new SyntaxError(`expected a name, found "${token.value}"`);
|
|
78
|
+
return token.value;
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
return cursor;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A type reference: `{ name, list, nonNull, item }`. */
|
|
85
|
+
export function parseTypeRef(cursor) {
|
|
86
|
+
let type;
|
|
87
|
+
if (cursor.take('[')) {
|
|
88
|
+
const item = parseTypeRef(cursor);
|
|
89
|
+
cursor.expect(']');
|
|
90
|
+
type = { list: true, item, name: item.name, nonNull: false };
|
|
91
|
+
} else {
|
|
92
|
+
type = { list: false, name: cursor.name(), nonNull: false };
|
|
93
|
+
}
|
|
94
|
+
if (cursor.take('!')) type = { ...type, nonNull: true };
|
|
95
|
+
return type;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Skips one value of any shape (a default value, a directive argument). */
|
|
99
|
+
export function skipValue(cursor) {
|
|
100
|
+
const token = cursor.next();
|
|
101
|
+
if (token.kind !== 'punct') return;
|
|
102
|
+
if (token.value === '$') return void cursor.name();
|
|
103
|
+
let close = null;
|
|
104
|
+
if (token.value === '{') close = '}';
|
|
105
|
+
else if (token.value === '[') close = ']';
|
|
106
|
+
if (close === null) return;
|
|
107
|
+
while (!cursor.take(close)) {
|
|
108
|
+
if (token.value === '{') {
|
|
109
|
+
cursor.name();
|
|
110
|
+
cursor.expect(':');
|
|
111
|
+
}
|
|
112
|
+
skipValue(cursor);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function skipDirectives(cursor) {
|
|
117
|
+
while (cursor.take('@')) {
|
|
118
|
+
cursor.name();
|
|
119
|
+
if (cursor.take('(')) {
|
|
120
|
+
while (!cursor.take(')')) {
|
|
121
|
+
cursor.name();
|
|
122
|
+
cursor.expect(':');
|
|
123
|
+
skipValue(cursor);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function skipDescription(cursor) {
|
|
130
|
+
if (cursor.peek()?.kind === 'string') cursor.next();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Field or input-value definitions between braces. */
|
|
134
|
+
function parseFieldDefinitions(cursor, withArguments) {
|
|
135
|
+
const fields = new Map();
|
|
136
|
+
cursor.expect('{');
|
|
137
|
+
while (!cursor.take('}')) {
|
|
138
|
+
skipDescription(cursor);
|
|
139
|
+
const name = cursor.name();
|
|
140
|
+
const args = new Map();
|
|
141
|
+
if (withArguments && cursor.take('(')) {
|
|
142
|
+
while (!cursor.take(')')) {
|
|
143
|
+
skipDescription(cursor);
|
|
144
|
+
const argName = cursor.name();
|
|
145
|
+
cursor.expect(':');
|
|
146
|
+
const type = parseTypeRef(cursor);
|
|
147
|
+
const hasDefault = cursor.take('=');
|
|
148
|
+
if (hasDefault) skipValue(cursor);
|
|
149
|
+
skipDirectives(cursor);
|
|
150
|
+
args.set(argName, { type, hasDefault });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
cursor.expect(':');
|
|
154
|
+
const type = parseTypeRef(cursor);
|
|
155
|
+
const hasDefault = cursor.take('=');
|
|
156
|
+
if (hasDefault) skipValue(cursor);
|
|
157
|
+
skipDirectives(cursor);
|
|
158
|
+
fields.set(name, { type, args, hasDefault });
|
|
159
|
+
}
|
|
160
|
+
return fields;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// `schema { query: Q ... }` (or `extend schema`): the root operation type names.
|
|
164
|
+
function readSchemaDefinition(cursor, keyword, roots) {
|
|
165
|
+
if (keyword === 'extend') cursor.name();
|
|
166
|
+
skipDirectives(cursor);
|
|
167
|
+
cursor.expect('{');
|
|
168
|
+
while (!cursor.take('}')) {
|
|
169
|
+
const operation = cursor.name();
|
|
170
|
+
cursor.expect(':');
|
|
171
|
+
roots[operation] = cursor.name();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// `directive @name(args) repeatable on A | B`: read and dropped.
|
|
176
|
+
function skipDirectiveDefinition(cursor) {
|
|
177
|
+
cursor.expect('@');
|
|
178
|
+
cursor.name();
|
|
179
|
+
if (cursor.take('(')) parseArgumentList(cursor);
|
|
180
|
+
if (cursor.is('repeatable')) cursor.name();
|
|
181
|
+
cursor.name();
|
|
182
|
+
cursor.take('|');
|
|
183
|
+
cursor.name();
|
|
184
|
+
while (cursor.take('|')) cursor.name();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// The type readers by keyword: each returns [name, definition] for the types map.
|
|
188
|
+
function readScalar(cursor) {
|
|
189
|
+
const name = cursor.name();
|
|
190
|
+
skipDirectives(cursor);
|
|
191
|
+
return [name, { kind: 'scalar' }];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function readEnum(cursor) {
|
|
195
|
+
const name = cursor.name();
|
|
196
|
+
skipDirectives(cursor);
|
|
197
|
+
const values = new Set();
|
|
198
|
+
cursor.expect('{');
|
|
199
|
+
while (!cursor.take('}')) {
|
|
200
|
+
skipDescription(cursor);
|
|
201
|
+
values.add(cursor.name());
|
|
202
|
+
skipDirectives(cursor);
|
|
203
|
+
}
|
|
204
|
+
return [name, { kind: 'enum', values }];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function readUnion(cursor) {
|
|
208
|
+
const name = cursor.name();
|
|
209
|
+
skipDirectives(cursor);
|
|
210
|
+
cursor.expect('=');
|
|
211
|
+
cursor.take('|');
|
|
212
|
+
const members = [cursor.name()];
|
|
213
|
+
while (cursor.take('|')) members.push(cursor.name());
|
|
214
|
+
return [name, { kind: 'union', members }];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function readObjectLike(cursor, keyword) {
|
|
218
|
+
const name = cursor.name();
|
|
219
|
+
if (cursor.is('implements')) {
|
|
220
|
+
cursor.name();
|
|
221
|
+
cursor.take('&');
|
|
222
|
+
cursor.name();
|
|
223
|
+
while (cursor.take('&')) cursor.name();
|
|
224
|
+
}
|
|
225
|
+
skipDirectives(cursor);
|
|
226
|
+
let kind = 'object';
|
|
227
|
+
if (keyword === 'input') kind = 'input';
|
|
228
|
+
else if (keyword === 'interface') kind = 'interface';
|
|
229
|
+
return [name, { kind, fields: parseFieldDefinitions(cursor, kind !== 'input') }];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const TYPE_READERS = new Map([['scalar', readScalar], ['enum', readEnum], ['union', readUnion], ['type', readObjectLike], ['interface', readObjectLike], ['input', readObjectLike]]);
|
|
233
|
+
|
|
234
|
+
/** The types of an SDL source: Map(name -> { kind, fields? , values? , members? }), plus the root operation type names. */
|
|
235
|
+
export function parseSchema(source) {
|
|
236
|
+
const cursor = cursorOf(tokenize(source));
|
|
237
|
+
const types = new Map();
|
|
238
|
+
const roots = { query: 'Query', mutation: 'Mutation', subscription: 'Subscription' };
|
|
239
|
+
while (!cursor.done()) {
|
|
240
|
+
skipDescription(cursor);
|
|
241
|
+
const keyword = cursor.name();
|
|
242
|
+
const readType = TYPE_READERS.get(keyword);
|
|
243
|
+
if (keyword === 'schema' || (keyword === 'extend' && cursor.is('schema'))) readSchemaDefinition(cursor, keyword, roots);
|
|
244
|
+
else if (keyword === 'directive') skipDirectiveDefinition(cursor);
|
|
245
|
+
else if (readType) {
|
|
246
|
+
const [name, definition] = readType(cursor, keyword);
|
|
247
|
+
types.set(name, definition);
|
|
248
|
+
} else throw new SyntaxError(`unexpected "${keyword}" in a schema`);
|
|
249
|
+
}
|
|
250
|
+
return { types, roots };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function parseArgumentList(cursor) {
|
|
254
|
+
while (!cursor.take(')')) {
|
|
255
|
+
skipDescription(cursor);
|
|
256
|
+
cursor.name();
|
|
257
|
+
cursor.expect(':');
|
|
258
|
+
parseTypeRef(cursor);
|
|
259
|
+
if (cursor.take('=')) skipValue(cursor);
|
|
260
|
+
skipDirectives(cursor);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// A placeholder is `{name}`; the English and the Vietnamese of one entry carry exactly the same placeholders
|
|
12
12
|
// (scripts/checks/check-i18n-catalog.mjs). An English source with no entry is returned as is: a missing translation degrades
|
|
13
13
|
// to English, never to a blank.
|
|
14
|
+
import { byCodeUnit } from './list.mjs';
|
|
14
15
|
import { escapeRegExp } from './regex.mjs';
|
|
15
16
|
import fs from 'node:fs';
|
|
16
17
|
import path from 'node:path';
|
|
@@ -19,7 +20,7 @@ import { parseYaml } from '../../engine/yaml.mjs';
|
|
|
19
20
|
import { DEFAULT_OWNER_LANGUAGE, loadConfig } from '../../engine/config.mjs';
|
|
20
21
|
|
|
21
22
|
export const CATALOG_DIR = 'modules/i18n/messages';
|
|
22
|
-
export const PLACEHOLDER = /\{([A-Za-z_]
|
|
23
|
+
export const PLACEHOLDER = /\{([A-Za-z_]\w*)\}/g;
|
|
23
24
|
|
|
24
25
|
const cache = new Map();
|
|
25
26
|
|
|
@@ -54,7 +55,7 @@ export const resetCatalogCache = () => cache.clear();
|
|
|
54
55
|
/** The names each `pattern` match captures (group 1) in `text`, in match order (`sort` sorts them). */
|
|
55
56
|
export const captureNames = (text, pattern, { sort = false } = {}) => {
|
|
56
57
|
const names = [...String(text).matchAll(pattern)].map((m) => m[1]);
|
|
57
|
-
return sort ? names.sort() : names;
|
|
58
|
+
return sort ? names.sort(byCodeUnit) : names;
|
|
58
59
|
};
|
|
59
60
|
|
|
60
61
|
/** The names of the placeholders of a message. */
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// in-order.mjs — asynchronous work done strictly one after another, named by what it does. A sequential `await` in a
|
|
2
|
+
// loop is often the point (ordered ledger writes, one Orca or git call at a time, a rate limit, stop at the first
|
|
3
|
+
// failure or match); these helpers state that intent at the call site so no loop body carries an `await`. Each one
|
|
4
|
+
// starts item n+1 only after item n has settled, lets the first rejection (or a synchronous throw of `fn`) stop the run
|
|
5
|
+
// with later items never started, pulls a lazy iterable one item at a time, and closes it on an early stop, the way
|
|
6
|
+
// `for (const x of xs) await fn(x)` does. All of them run on the one `walk` below: promise callbacks, not an async
|
|
7
|
+
// recursion, so the stack and the memory stay flat however long the input is.
|
|
8
|
+
// Provably independent work that needs no order, short-circuit or error contract may use `Promise.all` instead.
|
|
9
|
+
|
|
10
|
+
/** Close `iterator` the way a `for...of` leaves it: `return()` when it has one. */
|
|
11
|
+
const close = (iterator) => iterator.return?.();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Drive `visit(item, index)` over `items` one at a time. `decide(outcome, item)` runs after each settled visit: a
|
|
15
|
+
* `{ value }` box ends the walk with that value, anything else goes on. Resolves with the box value, or `undefined` when
|
|
16
|
+
* the items ran out; rejects with the first rejection or throw, after closing the iterator.
|
|
17
|
+
*/
|
|
18
|
+
const walk = (items, visit, decide) => new Promise((resolve, reject) => {
|
|
19
|
+
const iterator = items[Symbol.iterator]();
|
|
20
|
+
let index = 0;
|
|
21
|
+
const fail = (error) => {
|
|
22
|
+
try { close(iterator); } catch { /* the failure being reported wins, as it does in a for...of */ }
|
|
23
|
+
reject(error);
|
|
24
|
+
};
|
|
25
|
+
const stop = (value) => {
|
|
26
|
+
try { close(iterator); } catch (error) { reject(error); return; }
|
|
27
|
+
resolve(value);
|
|
28
|
+
};
|
|
29
|
+
const advance = () => {
|
|
30
|
+
let step, pending;
|
|
31
|
+
try {
|
|
32
|
+
step = iterator.next();
|
|
33
|
+
if (step.done) { resolve(undefined); return; }
|
|
34
|
+
pending = visit(step.value, index++);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (step === undefined || step.done) reject(error); else fail(error);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
Promise.resolve(pending).then((outcome) => {
|
|
40
|
+
let box;
|
|
41
|
+
try { box = decide(outcome, step.value); } catch (error) { fail(error); return; }
|
|
42
|
+
if (box) stop(box.value); else advance();
|
|
43
|
+
}, fail);
|
|
44
|
+
};
|
|
45
|
+
advance();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const never = () => undefined;
|
|
49
|
+
|
|
50
|
+
/** `for (const x of items) await fn(x, index)`: resolves undefined once every item has run, one at a time. */
|
|
51
|
+
export const eachInOrder = (items, fn) => walk(items, fn, never);
|
|
52
|
+
|
|
53
|
+
/** The sequential map: `fn(x, index)` for each item in turn, resolving the results in item order. */
|
|
54
|
+
export const mapInOrder = (items, fn) => {
|
|
55
|
+
const results = [];
|
|
56
|
+
return walk(items, fn, (outcome) => { results.push(outcome); }).then(() => results);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** The first item whose `predicate(x, index)` resolves truthy (undefined when none); later items never run. */
|
|
60
|
+
export const findInOrder = (items, predicate) => walk(items, predicate, (outcome, item) => (outcome ? { value: item } : undefined));
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* `for (;;) await step(attempt)`: `step(attempt)` runs again, after the previous run settled, until it resolves
|
|
64
|
+
* something other than undefined; that value (null included) is the result. A poll or retry returns undefined to go
|
|
65
|
+
* on and its answer to stop. `attempt` counts from 0.
|
|
66
|
+
*/
|
|
67
|
+
export const repeatInOrder = (step) => walk(forever(), step, (outcome) => (outcome === undefined ? undefined : { value: outcome }));
|
|
68
|
+
|
|
69
|
+
function* forever() {
|
|
70
|
+
for (let attempt = 0; ; attempt++) yield attempt;
|
|
71
|
+
}
|
|
@@ -50,13 +50,13 @@ export const DECLARED_VIETNAMESE_FIELDS = Object.freeze({
|
|
|
50
50
|
'modules/ops/_labels.yaml': Object.freeze({ flowFields: Object.freeze(['vi']) }),
|
|
51
51
|
'modules/goal/archetypes.yaml': Object.freeze({
|
|
52
52
|
fields: Object.freeze([
|
|
53
|
-
'buildIntent', 'canonIntent', 'e2eIntent', 'uatIntent', 'proofNegation', '
|
|
53
|
+
'buildIntent', 'canonIntent', 'e2eIntent', 'uatIntent', 'proofNegation', 'proofStateCue', 'integrationIntent', 'brandIntent',
|
|
54
54
|
'phrases', 'requires', 'excludes', 'backend', 'frontend', 'package',
|
|
55
55
|
]),
|
|
56
56
|
}),
|
|
57
57
|
'modules/goal/source-phrases.yaml': Object.freeze({
|
|
58
58
|
fields: Object.freeze([
|
|
59
|
-
'prefer', 'avoid', 'negation', 'refused', 'accept', 'golden',
|
|
59
|
+
'prefer', 'avoid', 'only', 'negation', 'refused', 'accept', 'golden',
|
|
60
60
|
'missingPaths', 'grantTooNarrow', 'toolTimeout', 'testGap', 'checkerUnavailable',
|
|
61
61
|
'supervisorVerbs', 'ownerWord', 'orWord', 'beyondAuthority', 'ownerOnly', 'workerDied', 'contractConflict', 'decision',
|
|
62
62
|
'product', 'grammar', 'knowledge', 'sourceLabel', 'internalVocabulary', 'actor', 'success',
|
|
@@ -124,7 +124,7 @@ export function documentLanguageHits(rel, text) {
|
|
|
124
124
|
if (declared.fields?.includes(keys[hit.line - 1])) return false;
|
|
125
125
|
if (!declared.flowFields) return true;
|
|
126
126
|
// an inline flow map: drop the declared fields' values and judge what is left of the line
|
|
127
|
-
const rest = declared.flowFields.reduce((line, field) => line.replace(new RegExp(
|
|
127
|
+
const rest = declared.flowFields.reduce((line, field) => line.replace(new RegExp(String.raw`\b${field}\s*:\s*(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^,}]*)`, 'g'), ''), lines[hit.line - 1]);
|
|
128
128
|
return hasSecondLanguage(rest);
|
|
129
129
|
});
|
|
130
130
|
}
|
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
export const list = (v) => (Array.isArray(v) ? v : []);
|
|
5
5
|
|
|
6
6
|
/** `v` as an array: itself when already one, [] for null/undefined, else [v]. */
|
|
7
|
-
export const asList = (v) =>
|
|
7
|
+
export const asList = (v) => {
|
|
8
|
+
if (Array.isArray(v)) return v;
|
|
9
|
+
if (v == null) return [];
|
|
10
|
+
return [v];
|
|
11
|
+
};
|
|
8
12
|
|
|
9
13
|
/** The object entries of `v` (a non-array reads as []). */
|
|
10
14
|
export const objectList = (v) => (Array.isArray(v) ? v : []).filter((item) => item && typeof item === 'object');
|
|
@@ -18,3 +22,6 @@ export const splitList = (v, { sep = ',', dedupe = false } = {}) => {
|
|
|
18
22
|
const items = String(v ?? '').split(sep).map((s) => s.trim()).filter(Boolean);
|
|
19
23
|
return dedupe ? [...new Set(items)] : items;
|
|
20
24
|
};
|
|
25
|
+
|
|
26
|
+
// The comparator of a bare `sort()` (UTF-16 code-unit order) is owned by the engine, which cannot import scripts/lib.
|
|
27
|
+
export { byCodeUnit } from '../../engine/by-code-unit.mjs';
|
|
@@ -6,6 +6,17 @@ const WIN = process.platform === 'win32';
|
|
|
6
6
|
|
|
7
7
|
/** `p` with every backslash turned into a forward slash; null and undefined read as ''. */
|
|
8
8
|
export const slash = (p) => String(p ?? '').replaceAll('\\', '/');
|
|
9
|
+
/** `text` without its trailing '/' run (a loop: a trailing-run pattern backtracks super-linearly). */
|
|
10
|
+
export const trimTrailingSlashes = (text) => {
|
|
11
|
+
for (let end = text.length; ; end -= 1) if (end === 0 || text[end - 1] !== '/') return text.slice(0, end);
|
|
12
|
+
};
|
|
13
|
+
// A trailing '*' run with the one '/' before it (the glob suffix of 'dir/**'); text without a trailing '*' is unchanged.
|
|
14
|
+
const stripTrailingStars = (text) => {
|
|
15
|
+
let end = text.length;
|
|
16
|
+
while (end > 0 && text[end - 1] === '*') end -= 1;
|
|
17
|
+
if (end === text.length) return text;
|
|
18
|
+
return text.slice(0, end > 0 && text[end - 1] === '/' ? end - 1 : end);
|
|
19
|
+
};
|
|
9
20
|
/** A relative path in git's spelling: forward slashes, no leading './'. */
|
|
10
21
|
export const posixPath = (p) => slash(p).replace(/^\.\//, '');
|
|
11
22
|
/** Whether posix `file` is `prefix` itself or under it (`file === prefix || file` starts with `prefix/`). */
|
|
@@ -19,7 +30,7 @@ export const samePath = (a, b) => foldCase(a) === foldCase(b);
|
|
|
19
30
|
* `fold: true` folds case on every platform (a comparison against a lowercase-stored root needs it).
|
|
20
31
|
*/
|
|
21
32
|
export const pathKey = (p, { fold = WIN } = {}) => {
|
|
22
|
-
const key = slash(path.resolve(p))
|
|
33
|
+
const key = trimTrailingSlashes(slash(path.resolve(p)));
|
|
23
34
|
return fold ? key.toLowerCase() : key;
|
|
24
35
|
};
|
|
25
36
|
/** `a` is `b` or under it, or the reverse: two path spellings whose trees touch. */
|
|
@@ -36,19 +47,28 @@ export const normPath = (p, { trim = false, collapse = false, dot = false, glob
|
|
|
36
47
|
s = slash(s);
|
|
37
48
|
if (collapse) s = s.replace(/\/{2,}/g, '/');
|
|
38
49
|
if (dot) s = s.replace(/^\.\//, '');
|
|
39
|
-
if (glob === 'star') s = s
|
|
50
|
+
if (glob === 'star') s = stripTrailingStars(s);
|
|
40
51
|
else if (glob === 'double') s = s.replace(/\/\*\*$/, '');
|
|
41
|
-
s = s
|
|
52
|
+
s = trimTrailingSlashes(s);
|
|
42
53
|
return fold ? s.toLowerCase() : s;
|
|
43
54
|
};
|
|
44
55
|
/** A work-relative path in one spelling: forward slashes, no leading ./, no trailing slash. */
|
|
45
|
-
export const normWork = (p) => String(p ?? '').trim().replaceAll('\\', '/').replace(/^\.\/+/, '')
|
|
56
|
+
export const normWork = (p) => trimTrailingSlashes(String(p ?? '').trim().replaceAll('\\', '/').replace(/^\.\/+/, ''));
|
|
46
57
|
|
|
47
58
|
/** `p` resolved and dereferenced (realpath where the path exists) — the canonical spelling of a checkout dir. */
|
|
48
59
|
export const realPath = (p) => {
|
|
49
60
|
const resolved = path.resolve(p);
|
|
50
61
|
try { return fs.realpathSync.native(resolved); } catch { return resolved; }
|
|
51
62
|
};
|
|
63
|
+
/** `p` with its nearest existing ancestor dereferenced and the not-yet-created rest appended: a root's canonical spelling before it exists. */
|
|
64
|
+
export const canonicalPath = (p) => {
|
|
65
|
+
const rest = [];
|
|
66
|
+
for (let at = path.resolve(p); ; at = path.dirname(at)) {
|
|
67
|
+
try { return path.join(fs.realpathSync.native(at), ...rest); } catch { /* not created yet */ }
|
|
68
|
+
if (path.dirname(at) === at) return path.resolve(p);
|
|
69
|
+
rest.unshift(path.basename(at));
|
|
70
|
+
}
|
|
71
|
+
};
|
|
52
72
|
/** `p` resolved and case-folded where the filesystem ignores case — the Map key of an absolute path. */
|
|
53
73
|
export const resolvedKey = (p) => foldCase(path.resolve(p));
|
|
54
74
|
/** Whether `a` and `b` resolve to the same path (always case-folded, so differently-cased spellings count). */
|
|
@@ -61,12 +81,25 @@ export const insidePath = (root, file, { key = (p) => p, includeSelf = false } =
|
|
|
61
81
|
const rel = path.relative(key(root), key(file));
|
|
62
82
|
return (includeSelf || Boolean(rel)) && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
63
83
|
};
|
|
84
|
+
/**
|
|
85
|
+
* `candidate` (absolute, or relative to `base`) resolved and dereferenced, required to sit under `base` (or be it):
|
|
86
|
+
* the boundary check for a path an operator, an agent or another process hands in. `base` is canonicalised once and the
|
|
87
|
+
* candidate is judged below it, so `..` segments and links leading out are refused; returns the canonical path, or throws
|
|
88
|
+
* a PATH_OUTSIDE_BASE refusal (`label` names the argument in the message).
|
|
89
|
+
*/
|
|
90
|
+
export const containedPath = (base, candidate, { label = 'path' } = {}) => {
|
|
91
|
+
const root = canonicalPath(base);
|
|
92
|
+
const resolved = canonicalPath(path.resolve(root, String(candidate ?? '')));
|
|
93
|
+
const prefix = root.endsWith(path.sep) ? root : root + path.sep;
|
|
94
|
+
if (!insidePath(root, resolved, { includeSelf: true }) || (resolved !== root && !resolved.startsWith(prefix))) throw Object.assign(new Error(`${label} ${String(candidate)} is outside ${root} [PATH_OUTSIDE_BASE]`), { code: 'PATH_OUTSIDE_BASE' });
|
|
95
|
+
return resolved;
|
|
96
|
+
};
|
|
64
97
|
/**
|
|
65
98
|
* A path-ish value (a string or a {path} entry) in relative compare form: trimmed, forward slashes,
|
|
66
99
|
* no trailing '/**' glob or slashes, no leading './'; `fold` lowercases for a case-insensitive key.
|
|
67
100
|
*/
|
|
68
101
|
export const normRel = (p, { fold = false } = {}) => {
|
|
69
|
-
const s = String(typeof p === 'string' ? p : p?.path ?? '').trim().
|
|
102
|
+
const s = trimTrailingSlashes(String(typeof p === 'string' ? p : p?.path ?? '').trim().replaceAll('\\', '/').replace(/\/\*\*$/, '')).replace(/^\.\//, '');
|
|
70
103
|
return fold ? s.toLowerCase() : s;
|
|
71
104
|
};
|
|
72
105
|
/** Whether `file` equals or sits under one of `prefixes` (posix spellings; `dot` counts a '.' prefix as covering all). */
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// pid-alive.mjs - whether a process id names a live process: the one probe the host locks, the file claim and the machine store share.
|
|
2
|
+
|
|
3
|
+
/** True while `pid` names a live process (EPERM counts as alive). */
|
|
4
|
+
export const pidAlive = (pid) => {
|
|
5
|
+
if (!Number.isInteger(pid) || pid <= 0) { return false; }
|
|
6
|
+
try { process.kill(pid, 0); return true; } catch (error) { return error?.code === 'EPERM'; }
|
|
7
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// regex.mjs - building a RegExp from text.
|
|
2
2
|
|
|
3
3
|
/** `text` with every regular-expression metacharacter escaped, so it matches itself literally. */
|
|
4
|
-
export const escapeRegExp = (text) => String(text).replace(/[.*+?^${}()|[\]\\]/g,
|
|
4
|
+
export const escapeRegExp = (text) => String(text).replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
// carry CRLF): the one comparison the database rules use for a committed file against its base or against a regenerated one.
|
|
3
3
|
|
|
4
4
|
/** True when both texts exist and are equal once CRLF is folded to LF and trailing whitespace at the end is dropped. */
|
|
5
|
-
export const sameText = (a, b) => a !== null && b !== null && a.
|
|
5
|
+
export const sameText = (a, b) => a !== null && b !== null && a.replaceAll('\r\n', '\n').trimEnd() === b.replaceAll('\r\n', '\n').trimEnd();
|
|
@@ -15,7 +15,7 @@ export const FORBIDDEN_FILES = [
|
|
|
15
15
|
export const SECRET_PATTERNS = [
|
|
16
16
|
{ name: 'private-key-block', re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/ },
|
|
17
17
|
{ name: 'aws-access-key', re: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
18
|
-
{ name: 'github-token', re: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b|\bgithub_pat_
|
|
18
|
+
{ name: 'github-token', re: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b|\bgithub_pat_\w{50,}\b/ },
|
|
19
19
|
{ name: 'anthropic-key', re: /\bsk-ant-[A-Za-z0-9_-]{20,}/ },
|
|
20
20
|
{ name: 'openai-key', re: /\bsk-(?:proj-)?[A-Za-z0-9]{32,}\b/ },
|
|
21
21
|
{ name: 'slack-token', re: /\bxox[baprs]-[A-Za-z0-9-]{10,}/ },
|
|
@@ -28,7 +28,11 @@ export const SECRET_PATTERNS = [
|
|
|
28
28
|
// candidate; only this heuristic takes the exemption, never a provider-shaped token above.
|
|
29
29
|
// A spec file's keyword-assigned values are test inputs (a spec's sign-in
|
|
30
30
|
// form password and a mocked accessToken once refused a push): the heuristic skips spec files too.
|
|
31
|
-
{ name: 'assigned-secret', re:
|
|
31
|
+
{ name: 'assigned-secret', re: new RegExp([
|
|
32
|
+
String.raw`\b(?:password|passwd|secret|api[_-]?key|access[_-]?token|client[_-]?secret)`,
|
|
33
|
+
String.raw`\b\s*[:=]\s*['"](?!env\([A-Z_][A-Z0-9_]*\)['"])([^'"\s$<{]{12,})`,
|
|
34
|
+
`['"]`,
|
|
35
|
+
].join(''), 'i'), placeholder: /fixture|stub|fake|dummy|placeholder|example|sample|changeme|redacted|mock/i,
|
|
32
36
|
skipFile: /\.(?:spec|test|e2e-spec)\.[cm]?[jt]sx?$/ },
|
|
33
37
|
];
|
|
34
38
|
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Test seam: STARCI_SLEEP_SCALE (a number >= 0, default 1) multiplies every wait. A spec that drives a real
|
|
5
5
|
* dispatch/boot against a fake host sets it small so the card's settle/attestation windows (seconds of pure
|
|
6
|
-
* waiting, counted logically by their callers, not by the wall clock) cost milliseconds and the run
|
|
7
|
-
*
|
|
6
|
+
* waiting, counted logically by their callers, not by the wall clock) cost milliseconds and the run does not
|
|
7
|
+
* depend on machine load. Unset in production.
|
|
8
8
|
*/
|
|
9
9
|
import { readEnv } from './env.mjs';
|
|
10
10
|
const scaleOf = () => {
|