@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
|
@@ -3,17 +3,88 @@
|
|
|
3
3
|
import { isPlainObject } from '../../engine/plain-object.mjs';
|
|
4
4
|
import { APP_KIND, connectionShapeProblems, NAME, RUNTIME_KIND, SLOT_ID } from './manifest-shape.mjs';
|
|
5
5
|
import { kindShapeProblems, patternShapeProblems } from './declaration-slots.mjs';
|
|
6
|
+
import { byCodeUnit } from '../lib/list.mjs';
|
|
6
7
|
|
|
7
8
|
/** Shape problems of the optional hfs.json `supabase` block whose auth posture DB_CONFIG_POLICY checks. */
|
|
9
|
+
const isSupabaseStringList = (value) => Array.isArray(value) && value.every((entry) => typeof entry === 'string' && entry.trim());
|
|
10
|
+
|
|
11
|
+
function supabaseValueProblems(block) {
|
|
12
|
+
const bad = [];
|
|
13
|
+
if (block.enableSignup !== undefined && typeof block.enableSignup !== 'boolean') bad.push('supabase.enableSignup must be true or false');
|
|
14
|
+
if (block.jwtExpiry !== undefined && !(Number.isInteger(block.jwtExpiry) && block.jwtExpiry > 0 && block.jwtExpiry <= 3600)) bad.push('supabase.jwtExpiry must be an integer of seconds, 3600 at most');
|
|
15
|
+
if (block.siteUrl !== undefined && (typeof block.siteUrl !== 'string' || !block.siteUrl.trim())) bad.push('supabase.siteUrl must be a URL string');
|
|
16
|
+
for (const key of ['redirectUrls', 'forceRls']) if (block[key] !== undefined && !isSupabaseStringList(block[key])) bad.push(`supabase.${key} must be a list of strings`);
|
|
17
|
+
return bad;
|
|
18
|
+
}
|
|
19
|
+
|
|
8
20
|
function supabaseBlockProblems(block) {
|
|
9
21
|
if (block === undefined) return [];
|
|
10
22
|
if (!isPlainObject(block)) return ['supabase must be an object'];
|
|
11
23
|
const bad = [];
|
|
12
24
|
for (const key of Object.keys(block)) if (!['enableSignup', 'jwtExpiry', 'siteUrl', 'redirectUrls', 'forceRls'].includes(key)) bad.push(`supabase has unknown key ${key}`);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
25
|
+
bad.push(...supabaseValueProblems(block));
|
|
26
|
+
return bad;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function appShapeProblems(apps, at, side, names) {
|
|
30
|
+
const bad = [];
|
|
31
|
+
if (!Array.isArray(apps) || !apps.length) {
|
|
32
|
+
bad.push(`${at}.apps must list every ${side}/apps/<name> with its kind`);
|
|
33
|
+
return bad;
|
|
34
|
+
}
|
|
35
|
+
apps.forEach((app, index) => {
|
|
36
|
+
if (!isPlainObject(app) || !NAME.test(String(app.name)) || !NAME.test(String(app.kind)) || Object.keys(app).some((key) => key !== 'name' && key !== 'kind')) bad.push(`${at}.apps[${index}] must be {name, kind}`);
|
|
37
|
+
else if (names.has(app.name)) bad.push(`app ${app.name} is declared twice (${names.get(app.name)} and ${side})`);
|
|
38
|
+
else names.set(app.name, side);
|
|
39
|
+
});
|
|
40
|
+
return bad;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function connectionProblems(side, value) {
|
|
44
|
+
const bad = [];
|
|
45
|
+
if (side !== 'be') {
|
|
46
|
+
bad.push('connections belong to the be side');
|
|
47
|
+
return bad;
|
|
48
|
+
}
|
|
49
|
+
const list = Array.isArray(value.connections) ? value.connections : null;
|
|
50
|
+
const shape = connectionShapeProblems(list, value.apps);
|
|
51
|
+
if (shape.length) {
|
|
52
|
+
bad.push(...shape);
|
|
53
|
+
return bad;
|
|
54
|
+
}
|
|
55
|
+
if (new Set(list.map((connection) => connection.name)).size !== list.length) bad.push('connections names must be unique');
|
|
56
|
+
for (const a of list) for (const b of list) if (a !== b && `${b.envPrefix}_`.startsWith(`${a.envPrefix}_`)) bad.push(`connections ${a.name} and ${b.name} share env keys (${a.envPrefix}_ covers ${b.envPrefix}_)`);
|
|
57
|
+
return bad;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Whether a declared list is present but not a list of distinct entries that all satisfy `valid`. */
|
|
61
|
+
const isBadUniqueList = (list, valid) => list !== undefined && (!Array.isArray(list) || !list.every((entry) => valid(entry)) || new Set(list).size !== list.length);
|
|
62
|
+
|
|
63
|
+
function declarationSideProblems(side, value, names) {
|
|
64
|
+
const bad = [];
|
|
65
|
+
const at = `sides.${side}`;
|
|
66
|
+
if (!isPlainObject(value)) return [`${at} must be an object`];
|
|
67
|
+
for (const key of Object.keys(value)) if (!['apps', 'optionalSlots', 'patterns', 'kinds', 'connections', 'reads'].includes(key)) bad.push(`${at} has unknown key ${key}`);
|
|
68
|
+
bad.push(...appShapeProblems(value.apps, at, side, names));
|
|
69
|
+
if (isBadUniqueList(value.optionalSlots, (slot) => SLOT_ID.test(String(slot)))) bad.push(`${at}.optionalSlots must be a unique list of slot ids`);
|
|
70
|
+
bad.push(...patternShapeProblems(value, at, NAME), ...kindShapeProblems(value, at, NAME));
|
|
71
|
+
if (isBadUniqueList(value.reads, (read) => typeof read === 'string' && read.length > 0)) bad.push(`${at}.reads must be a unique list of paths`);
|
|
72
|
+
if (value.connections !== undefined) bad.push(...connectionProblems(side, value));
|
|
73
|
+
return bad;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const DECLARATION_KEYS = new Set(['hfs', 'kind', 'project', 'edition', 'supabase', 'sides', 'browser']);
|
|
77
|
+
const RUNTIME_DECLARATION_KEYS = new Set(['hfs', 'kind', 'project']);
|
|
78
|
+
|
|
79
|
+
function productDeclarationProblems(declaration, profiles) {
|
|
80
|
+
const bad = [];
|
|
81
|
+
for (const key of Object.keys(declaration)) if (!DECLARATION_KEYS.has(key)) bad.push(`unknown key ${key}`);
|
|
82
|
+
bad.push(...supabaseBlockProblems(declaration.supabase));
|
|
83
|
+
if (declaration.browser !== undefined && declaration.browser !== true) bad.push('browser is `true` when the app has a browser journey (slot app.browser), and is left out otherwise');
|
|
84
|
+
if (declaration.kind !== APP_KIND) bad.push(`kind must be ${APP_KIND} (a product is one app repository with a be and an fe side) or ${RUNTIME_KIND} (the StarCi runtime repository)`);
|
|
85
|
+
if (!isPlainObject(declaration.sides) || Object.keys(declaration.sides).sort(byCodeUnit).join() !== profiles.join()) { bad.push('sides must declare exactly be and fe'); return bad; }
|
|
86
|
+
const names = new Map();
|
|
87
|
+
for (const side of profiles) bad.push(...declarationSideProblems(side, declaration.sides[side], names));
|
|
17
88
|
return bad;
|
|
18
89
|
}
|
|
19
90
|
|
|
@@ -24,39 +95,9 @@ export function declarationShapeProblems(declaration, profiles) {
|
|
|
24
95
|
if (!(Number.isInteger(declaration.hfs) && declaration.hfs >= 1)) bad.push('hfs must be the pinned manifest major (an integer, 1 or more)');
|
|
25
96
|
if (!NAME.test(String(declaration.project))) bad.push('project must be a project name');
|
|
26
97
|
if (declaration.kind === RUNTIME_KIND) {
|
|
27
|
-
for (const key of Object.keys(declaration)) if (!
|
|
98
|
+
for (const key of Object.keys(declaration)) if (!RUNTIME_DECLARATION_KEYS.has(key)) bad.push(`unknown key ${key} (a runtime declaration is {hfs, kind, project})`);
|
|
28
99
|
return bad;
|
|
29
100
|
}
|
|
30
|
-
|
|
31
|
-
bad.push(...supabaseBlockProblems(declaration.supabase));
|
|
32
|
-
if (declaration.browser !== undefined && declaration.browser !== true) bad.push('browser is `true` when the app has a browser journey (slot app.browser), and is left out otherwise');
|
|
33
|
-
if (declaration.kind !== APP_KIND) bad.push(`kind must be ${APP_KIND} (a product is one app repository with a be and an fe side) or ${RUNTIME_KIND} (the StarCi runtime repository)`);
|
|
34
|
-
if (!isPlainObject(declaration.sides) || Object.keys(declaration.sides).sort().join() !== profiles.join()) { bad.push('sides must declare exactly be and fe'); return bad; }
|
|
35
|
-
const names = new Map();
|
|
36
|
-
for (const side of profiles) {
|
|
37
|
-
const value = declaration.sides[side];
|
|
38
|
-
const at = `sides.${side}`;
|
|
39
|
-
if (!isPlainObject(value)) { bad.push(`${at} must be an object`); continue; }
|
|
40
|
-
for (const key of Object.keys(value)) if (!['apps', 'optionalSlots', 'patterns', 'kinds', 'connections', 'reads'].includes(key)) bad.push(`${at} has unknown key ${key}`);
|
|
41
|
-
if (!Array.isArray(value.apps) || !value.apps.length) bad.push(`${at}.apps must list every ${side}/apps/<name> with its kind`);
|
|
42
|
-
else value.apps.forEach((app, index) => {
|
|
43
|
-
if (!isPlainObject(app) || !NAME.test(String(app.name)) || !NAME.test(String(app.kind)) || Object.keys(app).some((key) => key !== 'name' && key !== 'kind')) bad.push(`${at}.apps[${index}] must be {name, kind}`);
|
|
44
|
-
else if (names.has(app.name)) bad.push(`app ${app.name} is declared twice (${names.get(app.name)} and ${side})`);
|
|
45
|
-
else names.set(app.name, side);
|
|
46
|
-
});
|
|
47
|
-
if (value.optionalSlots !== undefined && (!Array.isArray(value.optionalSlots) || !value.optionalSlots.every((slot) => SLOT_ID.test(String(slot))) || new Set(value.optionalSlots).size !== value.optionalSlots.length)) bad.push(`${at}.optionalSlots must be a unique list of slot ids`);
|
|
48
|
-
bad.push(...patternShapeProblems(value, at, NAME), ...kindShapeProblems(value, at, NAME));
|
|
49
|
-
if (value.reads !== undefined && (!Array.isArray(value.reads) || !value.reads.every((read) => typeof read === 'string' && read.length > 0) || new Set(value.reads).size !== value.reads.length)) bad.push(`${at}.reads must be a unique list of paths`);
|
|
50
|
-
if (value.connections !== undefined) {
|
|
51
|
-
if (side !== 'be') { bad.push('connections belong to the be side'); continue; }
|
|
52
|
-
const list = Array.isArray(value.connections) ? value.connections : null;
|
|
53
|
-
const shape = connectionShapeProblems(list, value.apps);
|
|
54
|
-
if (shape.length) bad.push(...shape);
|
|
55
|
-
else {
|
|
56
|
-
if (new Set(list.map((connection) => connection.name)).size !== list.length) bad.push('connections names must be unique');
|
|
57
|
-
for (const a of list) for (const b of list) if (a !== b && `${b.envPrefix}_`.startsWith(`${a.envPrefix}_`)) bad.push(`connections ${a.name} and ${b.name} share env keys (${a.envPrefix}_ covers ${b.envPrefix}_)`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
101
|
+
bad.push(...productDeclarationProblems(declaration, profiles));
|
|
61
102
|
return bad;
|
|
62
103
|
}
|
|
@@ -8,7 +8,7 @@ export function optionalSlotProblems(manifest, side, s) {
|
|
|
8
8
|
const bad = [];
|
|
9
9
|
for (const id of s.optionalSlots ?? []) {
|
|
10
10
|
const slot = manifest.slots.find((candidate) => candidate.id === id);
|
|
11
|
-
if (!slot
|
|
11
|
+
if (!slot?.profiles.includes(side)) bad.push(`sides.${side}.optionalSlots names ${id}, which is not a ${side} slot`);
|
|
12
12
|
else if (slot.presence !== 'opt-in') bad.push(`sides.${side}.optionalSlots names ${id}, which is ${slot.presence}, not opt-in`);
|
|
13
13
|
else if (slot.appKind !== undefined) bad.push(`sides.${side}.optionalSlots names ${id}; an app of kind ${slot.appKind} enables it`);
|
|
14
14
|
}
|
|
@@ -20,8 +20,10 @@ export function effectiveSlot(slot, profile, edition) {
|
|
|
20
20
|
if (edition !== 'lite') return slot;
|
|
21
21
|
const { lite, litePresence, ...base } = slot;
|
|
22
22
|
const presence = litePresenceOf(slot, profile);
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
let tracked = slot.tracked;
|
|
24
|
+
if (presence === 'forbidden') tracked = 'external';
|
|
25
|
+
else if (tracked === 'external') tracked = 'tracked';
|
|
26
|
+
return { ...base, ...lite, presence, tracked, tests: 'none' };
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/** The template group starci app sync renders `slot` from under `edition`: `liteManagedBy` (else `managedBy`) in lite, `managedBy` otherwise; undefined when unmanaged. */
|
|
@@ -61,6 +63,6 @@ export function enforcerJudgedInEdition(rules, kind, id, edition = 'full') {
|
|
|
61
63
|
export function editionRuleParams(params, edition) {
|
|
62
64
|
const { lite, ...base } = params;
|
|
63
65
|
const merged = edition === 'lite' && lite ? { ...base, ...lite } : base;
|
|
64
|
-
const deepFreeze = (value) => { if (value && typeof value === 'object') Object.values(value).forEach(deepFreeze); return Object.freeze(value); };
|
|
66
|
+
const deepFreeze = (value) => { if (value && typeof value === 'object') { Object.values(value).forEach(deepFreeze); } return Object.freeze(value); };
|
|
65
67
|
return deepFreeze(structuredClone(merged));
|
|
66
68
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Linear-time text trimming and `<placeholder>` replacement for the HFS path and prose checks.
|
|
2
|
+
|
|
3
|
+
/** `text` without its trailing characters that are in `chars`. */
|
|
4
|
+
export function trimTrailingChars(text, chars) {
|
|
5
|
+
let last = text.length - 1;
|
|
6
|
+
while (last >= 0 && chars.includes(text[last])) last -= 1;
|
|
7
|
+
return text.slice(0, last + 1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** `text` with every `<name>` placeholder (a `<`, one or more characters other than `>`, a `>`) replaced by `replacement`. */
|
|
11
|
+
export function replacePlaceholders(text, replacement) {
|
|
12
|
+
let out = '';
|
|
13
|
+
let from = 0;
|
|
14
|
+
let open = text.indexOf('<');
|
|
15
|
+
while (open >= 0) {
|
|
16
|
+
const close = text.indexOf('>', open + 1);
|
|
17
|
+
if (close < 0) break;
|
|
18
|
+
if (close === open + 1) {
|
|
19
|
+
open = text.indexOf('<', close);
|
|
20
|
+
} else {
|
|
21
|
+
out += text.slice(from, open) + replacement;
|
|
22
|
+
from = close + 1;
|
|
23
|
+
open = text.indexOf('<', from);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return out + text.slice(from);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Whether `text` holds a `<name>` placeholder. */
|
|
30
|
+
export const hasPlaceholder = (text) => replacePlaceholders(text, '') !== text;
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
// (sides, app kinds) and refuses a manifest whole on any problem (HFS_MANIFEST_INVALID). Pure: every function takes a
|
|
5
5
|
// parsed manifest or slot and returns a list of problems in the words of modules/schemas/hfs-slots.schema.yaml.
|
|
6
6
|
import { isPlainObject } from '../../engine/plain-object.mjs';
|
|
7
|
-
import { stringList } from '../lib/list.mjs';
|
|
7
|
+
import { stringList, byCodeUnit } from '../lib/list.mjs';
|
|
8
8
|
|
|
9
9
|
export const SEMVER = /^(\d+)\.(\d+)\.(\d+)$/;
|
|
10
10
|
export const NAME = /^[a-z][a-z0-9-]*$/;
|
|
11
|
-
|
|
11
|
+
const ENV_PREFIX =/^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$/;
|
|
12
12
|
/** How a bounded context (a connection) is isolated: its own logical database, or its own schema of a shared one. */
|
|
13
13
|
const CONTEXT_ISOLATIONS = Object.freeze(["database", "schema"]);
|
|
14
14
|
/** The app kinds that may own a context: the services, never the migrate or cli apps that only run migrations. */
|
|
@@ -31,7 +31,7 @@ export function connectionShapeProblems(list, apps) {
|
|
|
31
31
|
const keys = (c) => isPlainObject(c) && NAME.test(String(c.name)) && ENV_PREFIX.test(String(c.envPrefix)) && NAME.test(String(c.owner)) && CONTEXT_ISOLATIONS.includes(c.isolation)
|
|
32
32
|
&& (c.provider === undefined || CONNECTION_PROVIDERS.includes(c.provider))
|
|
33
33
|
&& Object.keys(c).every((k) => ['name', 'envPrefix', 'owner', 'isolation', 'provider'].includes(k)) && ['name', 'envPrefix', 'owner', 'isolation'].every((k) => k in c);
|
|
34
|
-
if (!list
|
|
34
|
+
if (!list?.every(keys)) return [`connections must be a list of {name: kebab-case context name, envPrefix: UPPER_SNAKE prefix of its env keys, owner: the service app that owns the context, isolation: ${CONTEXT_ISOLATIONS.join(' | ')}, provider?: ${CONNECTION_PROVIDERS.join(' | ')}}`];
|
|
35
35
|
const owns = (c) => Array.isArray(apps) && apps.some((app) => isPlainObject(app) && app.name === c.owner && CONTEXT_OWNER_KINDS.includes(app.kind));
|
|
36
36
|
return list.filter((c) => !owns(c)).map((c) => `connections ${c.name} is owned by ${c.owner}, which is not a ${CONTEXT_OWNER_KINDS.join(' or ')} app declared in the same side`);
|
|
37
37
|
}
|
|
@@ -42,7 +42,7 @@ export const TRACKED = ['tracked', 'ignored', 'external'];
|
|
|
42
42
|
/** A runtime manifest adds `generated`: tracked, written only by the slot's generatedBy, judged by drift (RT_GENERATED_DRIFT). */
|
|
43
43
|
const RUNTIME_TRACKED = [...TRACKED, 'generated'];
|
|
44
44
|
export const TESTS = ['unit-beside', 'e2e', 'none'];
|
|
45
|
-
/** The coverage mode of a tracked slot of the be profile (scripts/hfs/coverage-scope.mjs
|
|
45
|
+
/** The coverage mode of a tracked slot of the be profile or of a runtime manifest (scripts/hfs/coverage-scope.mjs and scripts/hfs/runtime-coverage-scope.mjs derive the coverage consumers from it). */
|
|
46
46
|
export const COVERAGE = ['required', 'none'];
|
|
47
47
|
export const APP_KIND = 'app';
|
|
48
48
|
|
|
@@ -86,31 +86,54 @@ export const MANIFEST_KINDS = [APP_KIND, RUNTIME_KIND];
|
|
|
86
86
|
export const manifestKind = (m) => (isPlainObject(m) && m.kind !== undefined ? m.kind : APP_KIND);
|
|
87
87
|
const strList = (v) => stringList(v);
|
|
88
88
|
|
|
89
|
-
const APP_SLOT_KEYS = ['id', 'profiles', 'path', 'presence', 'tracked', 'tier', 'tests', 'owner', 'appKind', 'minInstances', 'requiredWhen', 'requiredInstances', 'requires', 'pattern', 'trigger', 'allows', 'forbids', 'layers', 'kinds', 'roles', 'composedBy', 'budget', 'managedBy', 'liteManagedBy', 'rules', 'goesTo', 'why', 'since', '
|
|
89
|
+
const APP_SLOT_KEYS = ['id', 'profiles', 'path', 'presence', 'tracked', 'tier', 'tests', 'owner', 'appKind', 'minInstances', 'requiredWhen', 'requiredInstances', 'requires', 'pattern', 'trigger', 'allows', 'forbids', 'layers', 'kinds', 'roles', 'composedBy', 'budget', 'managedBy', 'liteManagedBy', 'rules', 'goesTo', 'why', 'since', 'perConnection', 'parent', 'editions', 'litePresence', 'lite', 'provider', 'coverage', 'entries', 'outcomeHome', 'anonymousActions'];
|
|
90
90
|
/** The fields a slot's `lite` overlay may hold: the same keys it would carry in the slot body, resolved under edition lite. */
|
|
91
91
|
const LITE_OVERLAY_KEYS = ['path', 'requires', 'allows', 'forbids', 'minInstances', 'requiredInstances'];
|
|
92
92
|
/** A runtime slot has no app kind, side composition, layer or managed template; it may name the generator of a generated copy. */
|
|
93
|
-
const RUNTIME_SLOT_KEYS = ['id', 'profiles', 'path', 'presence', 'tracked', 'tier', 'tests', 'owner', 'minInstances', 'requires', 'allows', 'forbids', 'budget', 'rules', 'goesTo', 'why', 'since', '
|
|
93
|
+
const RUNTIME_SLOT_KEYS = ['id', 'profiles', 'path', 'presence', 'tracked', 'tier', 'tests', 'owner', 'minInstances', 'requires', 'allows', 'forbids', 'budget', 'rules', 'goesTo', 'why', 'since', 'generatedBy', 'parent', 'coverage'];
|
|
94
|
+
|
|
95
|
+
const isPositiveInt = (v) => Number.isInteger(v) && v >= 1;
|
|
96
|
+
const instancesShapeOk = (v) => isPlainObject(v) && Object.values(v).every((list) => strList(list) && list.length);
|
|
97
|
+
|
|
98
|
+
const generatedByInvalid = (generatedBy) => typeof generatedBy !== 'string' || !generatedBy || generatedBy.startsWith('/') || generatedBy.includes('..');
|
|
99
|
+
|
|
100
|
+
const profilesInvalid = (profiles, appScope, scopes) => !Array.isArray(profiles) || !profiles.length || !profiles.every((p) => scopes.includes(p)) || new Set(profiles).size !== profiles.length || (profiles.includes(appScope) && profiles.length !== 1);
|
|
101
|
+
|
|
102
|
+
function runtimeIdentityProblems(slot, at) {
|
|
103
|
+
const bad = [];
|
|
104
|
+
if (!RUNTIME_SLOT_ID.test(String(slot.id))) bad.push(`${at}: id must look like runtime.<name>`);
|
|
105
|
+
if (JSON.stringify(slot.profiles) !== JSON.stringify([RUNTIME_KIND])) bad.push(`${at}: profiles must be [runtime]`);
|
|
106
|
+
if (slot.tracked === 'generated' && generatedByInvalid(slot.generatedBy)) bad.push(`${at}: a generated slot names its generator (generatedBy, a repository-relative path)`);
|
|
107
|
+
if (slot.generatedBy !== undefined && slot.tracked !== 'generated') bad.push(`${at}: generatedBy belongs to a generated slot`);
|
|
108
|
+
return bad;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function appIdentityProblems(slot, at, appScope, scopes) {
|
|
112
|
+
const bad = [];
|
|
113
|
+
if (!SLOT_ID.test(String(slot.id))) bad.push(`${at}: id must look like be.transport.http`);
|
|
114
|
+
if (profilesInvalid(slot.profiles, appScope, scopes)) bad.push(`${at}: profiles must be [app] or a unique non-empty subset of be, fe`);
|
|
115
|
+
else if ((slot.profiles[0] === appScope) !== String(slot.id).startsWith(`${appScope}.`)) bad.push(`${at}: an app-root slot has profiles [app] and an id app.<name>, and only it`);
|
|
116
|
+
return bad;
|
|
117
|
+
}
|
|
94
118
|
|
|
95
119
|
/** Shape problems of one slot of a manifest of `kind` (app or runtime). */
|
|
96
|
-
|
|
120
|
+
function slotIdentityProblems(slot, runtime, slotKeys, at, appScope, scopes) {
|
|
97
121
|
const bad = [];
|
|
98
|
-
const runtime = kind === RUNTIME_KIND;
|
|
99
|
-
const slotKeys = new Set(runtime ? RUNTIME_SLOT_KEYS : APP_SLOT_KEYS);
|
|
100
|
-
const tracked = runtime ? RUNTIME_TRACKED : TRACKED;
|
|
101
|
-
const at = isPlainObject(slot) && typeof slot.id === 'string' ? `slot ${slot.id}` : `slots[${index}]`;
|
|
102
|
-
if (!isPlainObject(slot)) return [`${at} is not a map`];
|
|
103
122
|
for (const key of Object.keys(slot)) if (!slotKeys.has(key)) bad.push(`${at}: unknown field ${key}`);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
123
|
+
bad.push(...(runtime ? runtimeIdentityProblems(slot, at) : appIdentityProblems(slot, at, appScope, scopes)));
|
|
124
|
+
return bad;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function slotCoverageProblems(slot, at, runtime) {
|
|
128
|
+
const bad = [];
|
|
129
|
+
const measured = slot.tracked === 'tracked' && (runtime || (Array.isArray(slot.profiles) && slot.profiles.includes('be')));
|
|
130
|
+
if (measured && !COVERAGE.includes(slot.coverage)) bad.push(`${at}: coverage must be one of ${COVERAGE.join(', ')} (every tracked slot of ${runtime ? 'a runtime manifest' : 'the be profile'} declares it)`);
|
|
131
|
+
if (!measured && slot.coverage !== undefined) bad.push(`${at}: coverage belongs to a tracked slot of ${runtime ? 'a runtime manifest' : 'the be profile'} only`);
|
|
132
|
+
return bad;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function slotLocationProblems(slot, at, runtime, tracked) {
|
|
136
|
+
const bad = [];
|
|
114
137
|
if (typeof slot.path !== 'string' || !slot.path) bad.push(`${at}: path is required`);
|
|
115
138
|
if (slot.parent !== undefined && !/^[a-z][a-z0-9-]*(\.[a-z0-9-]+)+$/.test(String(slot.parent))) bad.push(`${at}: parent must be a slot id`);
|
|
116
139
|
if (!PRESENCE.includes(slot.presence)) bad.push(`${at}: presence must be one of ${PRESENCE.join(', ')}`);
|
|
@@ -118,41 +141,70 @@ export function slotProblems(slot, index, kind, { appScope = 'app', scopes = []
|
|
|
118
141
|
if (!NAME.test(String(slot.tier))) bad.push(`${at}: tier must be a tier name, none or inherit`);
|
|
119
142
|
if (!TESTS.includes(slot.tests)) bad.push(`${at}: tests must be one of ${TESTS.join(', ')}`);
|
|
120
143
|
if (slot.owner !== undefined && typeof slot.owner !== 'boolean') bad.push(`${at}: owner must be a boolean`);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (slot.minInstances !== undefined && !(Number.isInteger(slot.minInstances) && slot.minInstances >= 1)) bad.push(`${at}: minInstances must be a positive integer`);
|
|
130
|
-
if (slot.requiredWhen !== undefined && slot.requiredWhen !== 'connections') bad.push(`${at}: requiredWhen may only be connections`);
|
|
144
|
+
bad.push(...slotCoverageProblems(slot, at, runtime));
|
|
145
|
+
return bad;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const NAMED_SLOT_FIELDS = [['appKind', 'appKind must be a name'], ['trigger', 'trigger must be a trigger kind name'], ['pattern', 'pattern must be a pattern name']];
|
|
149
|
+
|
|
150
|
+
function slotInstanceProblems(slot, at) {
|
|
151
|
+
const bad = [];
|
|
131
152
|
if (slot.pattern !== undefined && !/^[a-z][a-z0-9-]*$/.test(String(slot.pattern))) bad.push(`${at}: pattern must be a kebab-case pattern name`);
|
|
132
|
-
if (slot.requiredInstances !== undefined && !(
|
|
153
|
+
if (slot.requiredInstances !== undefined && !instancesShapeOk(slot.requiredInstances)) bad.push(`${at}: requiredInstances must map a variable to a non-empty list of names`);
|
|
154
|
+
return bad;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function slotPatternProblems(slot, at) {
|
|
158
|
+
const bad = [];
|
|
159
|
+
for (const [key, what] of NAMED_SLOT_FIELDS) if (slot[key] !== undefined && !NAME.test(String(slot[key]))) bad.push(`${at}: ${what}`);
|
|
160
|
+
if (slot.minInstances !== undefined && !isPositiveInt(slot.minInstances)) bad.push(`${at}: minInstances must be a positive integer`);
|
|
161
|
+
if (slot.requiredWhen !== undefined && slot.requiredWhen !== 'connections') bad.push(`${at}: requiredWhen may only be connections`);
|
|
162
|
+
bad.push(...slotInstanceProblems(slot, at));
|
|
163
|
+
return bad;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const kindsInvalid = (slot) => {
|
|
167
|
+
if (slot.kinds === undefined || !strList(slot.kinds)) return false;
|
|
168
|
+
return !slot.kinds.length || new Set(slot.kinds).size !== slot.kinds.length || slot.kinds.some((k) => !NAME.test(k) || (slot.layers ?? []).includes(k));
|
|
169
|
+
};
|
|
170
|
+
const rolesValid = (roles) => isPlainObject(roles) && Object.keys(roles).length && Object.entries(roles).every(([role, file]) => NAME.test(role) && typeof file === 'string' && file && !file.includes('/'));
|
|
171
|
+
const composedByValid = (list) => strList(list) && list.length && new Set(list).size === list.length;
|
|
172
|
+
const budgetValid = (budget) => isPlainObject(budget) && Object.keys(budget).length && Object.values(budget).every(isPositiveInt);
|
|
173
|
+
|
|
174
|
+
function slotListProblems(slot, at) {
|
|
175
|
+
const bad = [];
|
|
133
176
|
for (const key of ['requires', 'allows', 'forbids', 'layers', 'kinds']) if (slot[key] !== undefined && !strList(slot[key])) bad.push(`${at}: ${key} must be a list of strings`);
|
|
134
|
-
if (
|
|
135
|
-
if (slot.roles !== undefined && !(
|
|
136
|
-
if (slot.composedBy !== undefined && !(
|
|
137
|
-
if (slot.budget !== undefined && !(
|
|
138
|
-
if (slot
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (slot.
|
|
148
|
-
|
|
149
|
-
&& (slot.lite.path === undefined || (typeof slot.lite.path === 'string' && slot.lite.path))
|
|
150
|
-
&& ['requires', 'allows', 'forbids'].every((key) => slot.lite[key] === undefined || strList(slot.lite[key]))
|
|
151
|
-
&& (slot.lite.minInstances === undefined || (Number.isInteger(slot.lite.minInstances) && slot.lite.minInstances >= 1))
|
|
152
|
-
&& (slot.lite.requiredInstances === undefined || (isPlainObject(slot.lite.requiredInstances) && Object.values(slot.lite.requiredInstances).every((v) => strList(v) && v.length)));
|
|
153
|
-
if (!ok) bad.push(`${at}: lite may hold only ${LITE_OVERLAY_KEYS.join(', ')}, each shaped as in the slot body`);
|
|
154
|
-
}
|
|
177
|
+
if (kindsInvalid(slot)) bad.push(`${at}: kinds must be a non-empty list of unique folder names that are not layers`);
|
|
178
|
+
if (slot.roles !== undefined && !rolesValid(slot.roles)) bad.push(`${at}: roles must map a role name to a file name`);
|
|
179
|
+
if (slot.composedBy !== undefined && !composedByValid(slot.composedBy)) bad.push(`${at}: composedBy must be a non-empty list of unique app kinds`);
|
|
180
|
+
if (slot.budget !== undefined && !budgetValid(slot.budget)) bad.push(`${at}: budget must map names to positive integers`);
|
|
181
|
+
for (const key of ['managedBy', 'liteManagedBy']) if (slot[key] !== undefined && !NAME.test(String(slot[key]))) bad.push(`${at}: ${key} must be a template id`);
|
|
182
|
+
return bad;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function slotAppOptionProblems(slot, at) {
|
|
186
|
+
const bad = [];
|
|
187
|
+
if (slot.editions !== undefined && !(Array.isArray(slot.editions) && slot.editions.length && slot.editions.every((e) => EDITIONS.includes(e)) && new Set(slot.editions).size === slot.editions.length)) bad.push(`${at}: editions must be a non-empty subset of ${EDITIONS.join(', ')}`);
|
|
188
|
+
if (slot.litePresence !== undefined && !(PRESENCE.includes(slot.litePresence) || (isPlainObject(slot.litePresence) && Object.keys(slot.litePresence).length && Object.entries(slot.litePresence).every(([side, value]) => ['be', 'fe'].includes(side) && PRESENCE.includes(value))))) bad.push(`${at}: litePresence must be a presence value or map sides to presence values`);
|
|
189
|
+
if (slot.provider !== undefined) {
|
|
190
|
+
if (!CONNECTION_PROVIDERS.includes(slot.provider)) bad.push(`${at}: provider must be one of ${CONNECTION_PROVIDERS.join(' | ')}`);
|
|
191
|
+
else if (slot.presence !== 'opt-in' || slot.appKind !== undefined) bad.push(`${at}: provider belongs to an opt-in slot that is not an app kind (a connection with that provider enables it)`);
|
|
155
192
|
}
|
|
193
|
+
return bad;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function slotLiteOverlayProblems(slot, at) {
|
|
197
|
+
if (slot.lite === undefined) return [];
|
|
198
|
+
const ok = isPlainObject(slot.lite) && Object.keys(slot.lite).length && Object.keys(slot.lite).every((key) => LITE_OVERLAY_KEYS.includes(key))
|
|
199
|
+
&& (slot.lite.path === undefined || (typeof slot.lite.path === 'string' && slot.lite.path))
|
|
200
|
+
&& ['requires', 'allows', 'forbids'].every((key) => slot.lite[key] === undefined || strList(slot.lite[key]))
|
|
201
|
+
&& (slot.lite.minInstances === undefined || (Number.isInteger(slot.lite.minInstances) && slot.lite.minInstances >= 1))
|
|
202
|
+
&& (slot.lite.requiredInstances === undefined || (isPlainObject(slot.lite.requiredInstances) && Object.values(slot.lite.requiredInstances).every((v) => strList(v) && v.length)));
|
|
203
|
+
return ok ? [] : [`${at}: lite may hold only ${LITE_OVERLAY_KEYS.join(', ')}, each shaped as in the slot body`];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function slotEntryProblems(slot, at) {
|
|
207
|
+
const bad = [];
|
|
156
208
|
if (slot.entries !== undefined) {
|
|
157
209
|
const listed = strList(slot.entries) && slot.entries.length > 0 && new Set(slot.entries).size === slot.entries.length && slot.entries.every(relPath);
|
|
158
210
|
if (!listed) bad.push(`${at}: entries must be a non-empty list of unique relative file paths`);
|
|
@@ -161,33 +213,53 @@ export function slotProblems(slot, index, kind, { appScope = 'app', scopes = []
|
|
|
161
213
|
}
|
|
162
214
|
if (slot.anonymousActions !== undefined && !(strList(slot.anonymousActions) && slot.anonymousActions.length > 0 && new Set(slot.anonymousActions).size === slot.anonymousActions.length && slot.anonymousActions.every(relPath) && typeof slot.path === 'string' && slot.path.endsWith('/'))) bad.push(`${at}: anonymousActions must be a non-empty list of unique relative file paths of a directory slot`);
|
|
163
215
|
if (slot.outcomeHome !== undefined && !(slot.outcomeHome === true && typeof slot.path === 'string' && slot.path && !slot.path.endsWith('/'))) bad.push(`${at}: outcomeHome is true and belongs to a file slot (the one file that declares the repository's Outcome union)`);
|
|
216
|
+
return bad;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function slotPolicyProblems(slot, at) {
|
|
220
|
+
const bad = [];
|
|
164
221
|
if (slot.rules !== undefined && !(Array.isArray(slot.rules) && slot.rules.every((r) => /^[A-Z][A-Z0-9_]*\*?$/.test(String(r))) && new Set(slot.rules).size === slot.rules.length)) bad.push(`${at}: rules must be unique rule ids`);
|
|
165
222
|
if (slot.perConnection !== undefined && !(slot.id === 'be.persistence' && strList(slot.perConnection) && slot.perConnection.length && new Set(slot.perConnection).size === slot.perConnection.length && slot.perConnection.every((name) => NAME.test(name)))) bad.push(`${at}: perConnection is a unique list of capability names, only on be.persistence (the platform capabilities whose tables exist on every connection that uses them)`);
|
|
166
223
|
if (slot.since !== undefined && !SEMVER.test(String(slot.since))) bad.push(`${at}: since must be a version`);
|
|
167
|
-
if (slot.retiredIn !== undefined && !(Number.isInteger(slot.retiredIn) && slot.retiredIn >= 2)) bad.push(`${at}: retiredIn must be a major`);
|
|
168
|
-
if (slot.retiredIn !== undefined && typeof slot.successor !== 'string') bad.push(`${at}: a retired slot names its successor`);
|
|
169
224
|
if (slot.tracked === 'external' && (typeof slot.goesTo !== 'string' || slot.presence !== 'forbidden')) bad.push(`${at}: an external slot is forbidden and says where it goes (goesTo)`);
|
|
170
225
|
if (slot.presence === 'forbidden' && slot.tracked !== 'external') bad.push(`${at}: a forbidden slot is external`);
|
|
171
226
|
return bad;
|
|
172
227
|
}
|
|
173
228
|
|
|
229
|
+
export function slotProblems(slot, index, kind, { appScope = 'app', scopes = [] } = {}) {
|
|
230
|
+
const runtime = kind === RUNTIME_KIND;
|
|
231
|
+
const slotKeys = new Set(runtime ? RUNTIME_SLOT_KEYS : APP_SLOT_KEYS);
|
|
232
|
+
const tracked = runtime ? RUNTIME_TRACKED : TRACKED;
|
|
233
|
+
const at = isPlainObject(slot) && typeof slot.id === 'string' ? `slot ${slot.id}` : `slots[${index}]`;
|
|
234
|
+
if (!isPlainObject(slot)) return [`${at} is not a map`];
|
|
235
|
+
const bad = slotIdentityProblems(slot, runtime, slotKeys, at, appScope, scopes);
|
|
236
|
+
bad.push(...slotLocationProblems(slot, at, runtime, tracked), ...slotPatternProblems(slot, at), ...slotListProblems(slot, at));
|
|
237
|
+
if (!runtime) bad.push(...slotAppOptionProblems(slot, at), ...slotLiteOverlayProblems(slot, at));
|
|
238
|
+
bad.push(...slotEntryProblems(slot, at), ...slotPolicyProblems(slot, at));
|
|
239
|
+
return bad;
|
|
240
|
+
}
|
|
241
|
+
|
|
174
242
|
/** The owners of an external call in a runtime manifest: an api system folder (`api/<system>`, `api/*` any system) or the DB tier. */
|
|
175
243
|
const INFRA_OWNER = /^(?:api\/(?:\*|[a-z][a-z0-9-]*)|engine\/db)$/;
|
|
176
|
-
const RUNTIME_PARAM_KEYS = ['fileLines', 'sourceRoots', 'infraOwners', 'baseWriteMembers', 'baseEnvSeams', 'apiContracts', 'sourceName', 'oneOffNames', 'sharedBasenames', 'generated', 'pinned', 'selfChecks'];
|
|
244
|
+
const RUNTIME_PARAM_KEYS = new Set(['fileLines', 'sourceRoots', 'infraOwners', 'baseWriteMembers', 'baseEnvSeams', 'apiContracts', 'sourceName', 'oneOffNames', 'sharedBasenames', 'generated', 'heldSecrets', 'pinned', 'selfChecks']);
|
|
177
245
|
const relPath = (v) => typeof v === 'string' && v.length > 0 && !v.startsWith('/') && !v.includes('..') && !v.includes('\\');
|
|
178
246
|
|
|
179
|
-
|
|
180
|
-
export function runtimeShapeProblems(m) {
|
|
247
|
+
function runtimeHeaderProblems(m) {
|
|
181
248
|
const bad = [];
|
|
182
249
|
const allowed = new Set(['schema', 'kind', 'version', 'versioning', 'presenceValues', 'trackedValues', 'testValues', 'profiles', 'tiers', 'ruleParams', 'crossOwner', 'crossSystem', 'slots', 'consumers']);
|
|
183
250
|
for (const key of Object.keys(m)) if (!allowed.has(key)) bad.push(`unknown top-level key ${key}`);
|
|
184
251
|
if (!/^starci\/runtime-slots@\d+$/.test(String(m.schema))) bad.push('schema must be starci/runtime-slots@<major>');
|
|
185
252
|
if (!SEMVER.test(String(m.version))) bad.push('version must be MAJOR.MINOR.PATCH');
|
|
186
|
-
if (!isPlainObject(m.versioning) || !['patch', 'minor', 'major', '
|
|
253
|
+
if (!isPlainObject(m.versioning) || !['patch', 'minor', 'major', 'pins'].every((k) => typeof m.versioning[k] === 'string')) bad.push('versioning needs patch, minor, major and pins text');
|
|
187
254
|
if (JSON.stringify(m.presenceValues) !== JSON.stringify(PRESENCE)) bad.push(`presenceValues must be ${PRESENCE.join(', ')}`);
|
|
188
255
|
if (JSON.stringify(m.trackedValues) !== JSON.stringify(RUNTIME_TRACKED)) bad.push(`trackedValues must be ${RUNTIME_TRACKED.join(', ')}`);
|
|
189
256
|
if (JSON.stringify(m.testValues) !== JSON.stringify(TESTS)) bad.push(`testValues must be ${TESTS.join(', ')}`);
|
|
190
257
|
if (JSON.stringify(m.profiles) !== JSON.stringify([RUNTIME_KIND])) bad.push('profiles must be [runtime]');
|
|
258
|
+
return bad;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function runtimeSectionsProblems(m) {
|
|
262
|
+
const bad = [];
|
|
191
263
|
for (const key of ['crossOwner', 'crossSystem']) if (m[key] !== undefined && typeof m[key] !== 'string') bad.push(`${key} must be text`);
|
|
192
264
|
if (!isPlainObject(m.tiers) || Object.keys(m.tiers).join() !== RUNTIME_KIND) bad.push('tiers must be a map with exactly runtime');
|
|
193
265
|
else bad.push(...tierMapProblems(RUNTIME_KIND, m.tiers.runtime));
|
|
@@ -200,19 +272,39 @@ export function runtimeShapeProblems(m) {
|
|
|
200
272
|
return bad;
|
|
201
273
|
}
|
|
202
274
|
|
|
275
|
+
/** Shape problems of a parsed manifest of kind runtime (knowledge/hfs/runtime-slots.yaml), in the words of modules/schemas/hfs-slots.schema.yaml. */
|
|
276
|
+
export function runtimeShapeProblems(m) {
|
|
277
|
+
return [...runtimeHeaderProblems(m), ...runtimeSectionsProblems(m)];
|
|
278
|
+
}
|
|
279
|
+
|
|
203
280
|
/** Shape problems of ruleParams.runtime. */
|
|
204
|
-
function
|
|
281
|
+
function runtimeParamKeyProblems(rp) {
|
|
205
282
|
const bad = [];
|
|
206
|
-
for (const key of Object.keys(rp)) if (!RUNTIME_PARAM_KEYS.
|
|
283
|
+
for (const key of Object.keys(rp)) if (!RUNTIME_PARAM_KEYS.has(key)) bad.push(`ruleParams.runtime.${key} is not a runtime parameter`);
|
|
207
284
|
for (const key of RUNTIME_PARAM_KEYS) if (!(key in rp)) bad.push(`ruleParams.runtime.${key} is missing`);
|
|
285
|
+
return bad;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function runtimeParamFoundationProblems(rp) {
|
|
289
|
+
const bad = [];
|
|
208
290
|
const fl = rp.fileLines;
|
|
209
291
|
if (!(isPlainObject(fl) && Number.isInteger(fl.soft) && fl.soft >= 1 && typeof fl.hardGrowth === 'boolean' && Object.keys(fl).length === 2)) bad.push('ruleParams.runtime.fileLines must be {soft, hardGrowth}');
|
|
210
292
|
const owners = rp.infraOwners;
|
|
211
293
|
const ownerMap = (v) => isPlainObject(v) && Object.entries(v).every(([key, list]) => key && Array.isArray(list) && list.every((o) => INFRA_OWNER.test(String(o))) && new Set(list).size === list.length);
|
|
212
|
-
if (!isPlainObject(owners) || Object.keys(owners).sort().join() !== 'globals,modules,programs' || !['globals', 'modules', 'programs'].every((k) => ownerMap(owners[k]))) bad.push('ruleParams.runtime.infraOwners must be {modules, globals, programs}, each mapping a name to unique owners api/<system>, api/* or engine/db ([] means nowhere)');
|
|
294
|
+
if (!isPlainObject(owners) || Object.keys(owners).sort(byCodeUnit).join() !== 'globals,modules,programs' || !['globals', 'modules', 'programs'].every((k) => ownerMap(owners[k]))) bad.push('ruleParams.runtime.infraOwners must be {modules, globals, programs}, each mapping a name to unique owners api/<system>, api/* or engine/db ([] means nowhere)');
|
|
295
|
+
return bad;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function runtimeParamListProblems(rp) {
|
|
299
|
+
const bad = [];
|
|
213
300
|
for (const key of ['sourceRoots', 'baseWriteMembers', 'oneOffNames', 'sharedBasenames']) if (!(strList(rp[key]) && rp[key].length && new Set(rp[key]).size === rp[key].length)) bad.push(`ruleParams.runtime.${key} must be a non-empty list of unique strings`);
|
|
214
301
|
if (!(Array.isArray(rp.baseEnvSeams) && rp.baseEnvSeams.every(relPath))) bad.push('ruleParams.runtime.baseEnvSeams must be a list of repository-relative paths');
|
|
215
302
|
if (!(isPlainObject(rp.apiContracts) && Object.entries(rp.apiContracts).every(([system, file]) => NAME.test(system) && relPath(file)))) bad.push('ruleParams.runtime.apiContracts must map an api system to its calls contract path');
|
|
303
|
+
return bad;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function runtimeParamArtifactProblems(rp) {
|
|
307
|
+
const bad = [];
|
|
216
308
|
let sourceNameOk = typeof rp.sourceName === 'string';
|
|
217
309
|
try { if (sourceNameOk) new RegExp(rp.sourceName); } catch { sourceNameOk = false; }
|
|
218
310
|
if (!sourceNameOk) bad.push('ruleParams.runtime.sourceName must be a regular expression');
|
|
@@ -222,6 +314,15 @@ function runtimeParamProblems(rp) {
|
|
|
222
314
|
return bad;
|
|
223
315
|
}
|
|
224
316
|
|
|
317
|
+
function runtimeParamProblems(rp) {
|
|
318
|
+
return [
|
|
319
|
+
...runtimeParamKeyProblems(rp),
|
|
320
|
+
...runtimeParamFoundationProblems(rp),
|
|
321
|
+
...runtimeParamListProblems(rp),
|
|
322
|
+
...runtimeParamArtifactProblems(rp),
|
|
323
|
+
];
|
|
324
|
+
}
|
|
325
|
+
|
|
225
326
|
/** Shape problems of the tier map of one profile. */
|
|
226
327
|
export function tierMapProblems(profile, tiers) {
|
|
227
328
|
const bad = [];
|