@starci/hfs 4.0.9 → 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/LICENSE +21 -0
- package/emit/operations.mjs +1 -1
- package/emit/type-schema.mjs +2 -1
- package/lint/run.mjs +82 -8
- package/package.json +3 -2
- 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 +247 -273
- 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 +134 -0
- package/runtime/engine/sonar-config.mjs +21 -0
- package/runtime/engine/temp-root.mjs +32 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +18 -18
- package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
- package/runtime/knowledge/hfs/rules.yaml +102 -69
- 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 -1
- package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
- package/runtime/modules/models/registry.yaml +25 -397
- package/runtime/modules/models/runtimes.yaml +92 -262
- 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/lib.mjs +6 -0
- 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 +36 -1
- package/runtime/scripts/api/process/resolve-real-tool.mjs +52 -0
- package/runtime/scripts/api/process/run-program.mjs +8 -0
- package/runtime/scripts/api/sops/decrypt.mjs +9 -16
- package/runtime/scripts/api/sops/lib.mjs +309 -11
- package/runtime/scripts/api/sops/seal.mjs +8 -4
- 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 +3 -2
- 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 +90 -45
- 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/fs-kind.mjs +14 -4
- 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/mutation-fence.mjs +15 -0
- package/runtime/scripts/lib/path-key.mjs +42 -9
- 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 +103 -2
- 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
|
@@ -25,7 +25,12 @@ export function testSecretPaths(name, { repo, stack = 'dev' } = {}) {
|
|
|
25
25
|
export const setCommand = (name = '<name>', stack = 'dev') => `node scripts/stack-secret.mjs set ${stack}/secrets/test/${name}`;
|
|
26
26
|
|
|
27
27
|
/** sops' format for a plaintext path, as the repository's stack-secret.mjs formatFor decides it. */
|
|
28
|
-
export const sopsFormatFor = (file) =>
|
|
28
|
+
export const sopsFormatFor = (file) => {
|
|
29
|
+
if (file.endsWith('.env')) return 'dotenv';
|
|
30
|
+
if (file.endsWith('.json')) return 'json';
|
|
31
|
+
if (/\.(ya?ml|kubeconfig)$/.test(file)) return 'yaml';
|
|
32
|
+
return 'binary';
|
|
33
|
+
};
|
|
29
34
|
|
|
30
35
|
/** Every leaf outside the `sops` block is an ENC[...] value (or null); the block itself carries a mac. */
|
|
31
36
|
const allEncrypted = (node) => {
|
|
@@ -48,7 +53,7 @@ export function isSopsEnvelope(text) {
|
|
|
48
53
|
if (!src.trim()) return false;
|
|
49
54
|
try { return sopsTree(JSON.parse(src)); } catch { /* not JSON */ }
|
|
50
55
|
const lines = src.split(/\r?\n/).filter((l) => l.trim() && !/^\s*#/.test(l));
|
|
51
|
-
if (lines.length && lines.every((l) => /^[A-Za-z_]
|
|
56
|
+
if (lines.length && lines.every((l) => /^[A-Za-z_]\w*=/.test(l))) {
|
|
52
57
|
const pairs = lines.map((l) => [l.slice(0, l.indexOf('=')), l.slice(l.indexOf('=') + 1)]);
|
|
53
58
|
const meta = pairs.filter(([k]) => k.startsWith('sops_'));
|
|
54
59
|
const vals = pairs.filter(([k]) => !k.startsWith('sops_'));
|
|
@@ -56,3 +61,99 @@ export function isSopsEnvelope(text) {
|
|
|
56
61
|
}
|
|
57
62
|
try { return sopsTree(parseYaml(src)); } catch { return false; }
|
|
58
63
|
}
|
|
64
|
+
|
|
65
|
+
/** Admit one flat age envelope for the native selected-identity invocation; this is shape admission, never cryptographic proof. */
|
|
66
|
+
const refusal = (reason) => ({ ok: false, reason });
|
|
67
|
+
|
|
68
|
+
function dotenvMetadataValue(meta, age, key, value) {
|
|
69
|
+
if (key === 'age__list_0__map_recipient') age.recipient = value;
|
|
70
|
+
else if (key === 'age__list_0__map_enc') age.enc = value;
|
|
71
|
+
else if (key === 'mac_only_encrypted') {
|
|
72
|
+
if (value !== 'false') return 'partial-mac';
|
|
73
|
+
meta[key] = false;
|
|
74
|
+
} else if (key.includes('__')) return 'unsupported-key-group';
|
|
75
|
+
else meta[key] = value;
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readDotenvMetadata(text) {
|
|
80
|
+
const pairs = new Map();
|
|
81
|
+
for (const line of String(text).split('\n')) {
|
|
82
|
+
if (!line || line.startsWith('#')) continue;
|
|
83
|
+
if (line.includes('\r')) return { refusal: 'unsupported-dotenv' };
|
|
84
|
+
const at = line.indexOf('=');
|
|
85
|
+
if (at < 1) return { refusal: 'unsupported-dotenv' };
|
|
86
|
+
const name = line.slice(0, at);
|
|
87
|
+
if (pairs.has(name)) return { refusal: 'duplicate-key' };
|
|
88
|
+
pairs.set(name, line.slice(at + 1).replaceAll(String.raw`\n`, '\n'));
|
|
89
|
+
}
|
|
90
|
+
const meta = {};
|
|
91
|
+
const age = {};
|
|
92
|
+
for (const [name, value] of pairs) {
|
|
93
|
+
if (!name.startsWith('sops_')) continue;
|
|
94
|
+
const reason = dotenvMetadataValue(meta, age, name.slice('sops_'.length), value);
|
|
95
|
+
if (reason) return { refusal: reason };
|
|
96
|
+
}
|
|
97
|
+
meta.age = [age];
|
|
98
|
+
return { meta };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function readStructuredMetadata(text, format) {
|
|
102
|
+
const doc = parseYaml(String(text));
|
|
103
|
+
if (format !== 'yaml') JSON.parse(String(text));
|
|
104
|
+
if (format === 'binary' && (!doc || Object.keys(doc).length !== 2 || !Object.hasOwn(doc, 'data') || !Object.hasOwn(doc, 'sops') || typeof doc.data !== 'string'))
|
|
105
|
+
return { refusal: 'unsupported-binary-shape' };
|
|
106
|
+
return { meta: doc?.sops };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readAgeMetadata(text, format) {
|
|
110
|
+
try {
|
|
111
|
+
return format === 'dotenv' ? readDotenvMetadata(text) : readStructuredMetadata(text, format);
|
|
112
|
+
} catch { return { refusal: 'invalid-envelope' }; }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function hasNonAgeProvider(meta, providers) {
|
|
116
|
+
for (const name of providers) {
|
|
117
|
+
if (Object.hasOwn(meta, name) && (!Array.isArray(meta[name]) || meta[name].length)) return true;
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isAgeEntry(entry) {
|
|
123
|
+
return !!entry && typeof entry === 'object' && !Array.isArray(entry)
|
|
124
|
+
&& !Object.keys(entry).some(name => !['recipient', 'enc'].includes(name));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function hasValidAgePayload(entry) {
|
|
128
|
+
return typeof entry.enc === 'string' && entry.enc.startsWith('-----BEGIN AGE ENCRYPTED FILE-----\n')
|
|
129
|
+
&& entry.enc.includes('\n-----END AGE ENCRYPTED FILE-----');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function hasValidEnvelopeVersion(meta) {
|
|
133
|
+
return typeof meta.version === 'string' && /^\d+\.\d+\.\d+$/.test(meta.version)
|
|
134
|
+
&& typeof meta.lastmodified === 'string' && Number.isFinite(Date.parse(meta.lastmodified));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function validateAgeMetadata(meta, text, recipient) {
|
|
138
|
+
if (!meta || typeof meta !== 'object' || Array.isArray(meta) || !isSopsEnvelope(text)) return refusal('invalid-envelope');
|
|
139
|
+
if (Object.hasOwn(meta, 'key_groups') || Object.hasOwn(meta, 'shamir_threshold')) return refusal('unsupported-key-group');
|
|
140
|
+
const providers = ['kms', 'gcp_kms', 'hckms', 'azure_kv', 'hc_vault', 'pgp'];
|
|
141
|
+
if (hasNonAgeProvider(meta, providers)) return refusal('non-age-provider');
|
|
142
|
+
const fields = new Set([...providers, 'age', 'lastmodified', 'mac', 'version', 'unencrypted_suffix', 'encrypted_suffix', 'unencrypted_regex', 'encrypted_regex', 'unencrypted_comment_regex', 'encrypted_comment_regex', 'mac_only_encrypted']);
|
|
143
|
+
if (Object.keys(meta).some(name => !fields.has(name))) return refusal('unknown-metadata');
|
|
144
|
+
if (Object.hasOwn(meta, 'mac_only_encrypted') && meta.mac_only_encrypted !== false) return refusal('partial-mac');
|
|
145
|
+
if (!Array.isArray(meta.age) || meta.age.length !== 1) return refusal('unsupported-age-set');
|
|
146
|
+
const entry = meta.age[0];
|
|
147
|
+
if (!isAgeEntry(entry)) return refusal('invalid-age-entry');
|
|
148
|
+
if (typeof entry.recipient !== 'string' || entry.recipient !== recipient) return refusal('recipient-mismatch');
|
|
149
|
+
if (!hasValidAgePayload(entry)) return refusal('invalid-age-entry');
|
|
150
|
+
if (!hasValidEnvelopeVersion(meta)) return refusal('invalid-envelope');
|
|
151
|
+
return { ok: true };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function selectedAgeEnvelope(text, format, recipient) {
|
|
155
|
+
if (!['binary', 'json', 'yaml', 'dotenv'].includes(format)) return refusal('unsupported-format');
|
|
156
|
+
const parsed = readAgeMetadata(text, format);
|
|
157
|
+
if (parsed.refusal) return refusal(parsed.refusal);
|
|
158
|
+
return validateAgeMetadata(parsed.meta, text, recipient);
|
|
159
|
+
}
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { parseYaml } from '../../engine/yaml.mjs';
|
|
4
4
|
import { words } from './dockerfile.mjs';
|
|
5
|
+
import { asList } from './list.mjs';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* The services of a repository's own stack definition, read from `.starcistacks/application-stacks.yaml` and the compose
|
|
@@ -74,8 +75,6 @@ const imagePathOf = image => {
|
|
|
74
75
|
/** True when the image reference carries a tag or digest (an external image, not a placeholder of the repository's own build). */
|
|
75
76
|
const isTagged = image => image.includes('@') || image.slice(image.lastIndexOf('/') + 1).includes(':');
|
|
76
77
|
|
|
77
|
-
const asList = value => (Array.isArray(value) ? value : value === undefined || value === null ? [] : [value]);
|
|
78
|
-
|
|
79
78
|
/** A compose `environment` (map or `KEY=VALUE` list) as a plain object; a `*_FILE` variable becomes the plain variable with the test value. */
|
|
80
79
|
const environmentOf = value => {
|
|
81
80
|
const entries = Array.isArray(value)
|
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
// literal text, a node's 1-based position, a named property's initializer text. `ts` is the caller's
|
|
3
3
|
// already-resolved compiler (see package-at.mjs loadTypescript); nothing here resolves a package.
|
|
4
4
|
|
|
5
|
+
/** Whether the single-bit `flag` (a ts.NodeFlags member) is set in `flags`: the bit read as arithmetic, no bitwise operator in the condition. */
|
|
6
|
+
export const hasFlag = (flags, flag) => Math.floor(flags / flag) % 2 === 1;
|
|
7
|
+
|
|
8
|
+
/** Whether any bit of `mask` (a ts flag or an OR of flags) is set in `flags` (absent flags read as none): an explicit comparison of the masked value. */
|
|
9
|
+
export const hasAnyFlag = (flags, mask) => ((flags ?? 0) & mask) !== 0;
|
|
10
|
+
|
|
11
|
+
/** Whether the variable `declaration` belongs to a `const` list: the TypeScript node-flag test, an explicit comparison of the masked flags. */
|
|
12
|
+
export const isConstVariable = (ts, declaration) => (ts.getCombinedNodeFlags(declaration.parent) & ts.NodeFlags.Const) !== 0;
|
|
13
|
+
|
|
5
14
|
/** The text of an identifier or a string-literal-like `node`, or null (property names, literal specifiers). */
|
|
6
15
|
export const nameText = (ts, node) => (node && (ts.isIdentifier(node) || ts.isStringLiteralLike(node)) ? node.text : null);
|
|
7
16
|
|
|
@@ -20,7 +20,12 @@ export function walkFiles(dir, {filter = () => true, sorted = false, exclude = (
|
|
|
20
20
|
if (depth > maxDepth) return;
|
|
21
21
|
let entries;
|
|
22
22
|
try { entries = fs.readdirSync(current, {withFileTypes: true}); }
|
|
23
|
-
catch (error) {
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (ignoreReadErrors) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
24
29
|
if (sorted) entries = entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
25
30
|
for (const entry of entries) {
|
|
26
31
|
const full = path.join(current, entry.name);
|
|
@@ -6,6 +6,7 @@ import { appScripts, imageFiles, TEMPLATES_DIR } from "../sync/index.mjs";
|
|
|
6
6
|
import { ScaffoldError } from "./service.mjs";
|
|
7
7
|
import { jsonText, packageJsonText } from "./app.mjs";
|
|
8
8
|
import { registerLiteExports } from "./lite-exports.mjs";
|
|
9
|
+
import { byCodeUnit } from "../report/order.mjs";
|
|
9
10
|
|
|
10
11
|
const read = (relative) =>
|
|
11
12
|
fs
|
|
@@ -204,7 +205,7 @@ export function ensureLiteCli({ root, manifest, repo }) {
|
|
|
204
205
|
declaration.sides.be.apps.push({ name: "cli", kind: "cli" });
|
|
205
206
|
declaration.sides.be.kinds = [
|
|
206
207
|
...new Set([...(declaration.sides.be.kinds ?? []), "cli"]),
|
|
207
|
-
].sort();
|
|
208
|
+
].sort(byCodeUnit);
|
|
208
209
|
const resolved = resolveRepoDeclaration(manifest, declaration);
|
|
209
210
|
const image = imageFiles(resolved).find(
|
|
210
211
|
(file) => file.path === "be/apps/cli/Dockerfile",
|
package/scaffold/add-table.mjs
CHANGED
|
@@ -316,7 +316,7 @@ export function addTable({
|
|
|
316
316
|
fs.writeFileSync(target, file.body);
|
|
317
317
|
created.push(target);
|
|
318
318
|
}
|
|
319
|
-
if (emitTypes
|
|
319
|
+
if (typeof emitTypes === "function") {
|
|
320
320
|
const generated = emitTypes({ root });
|
|
321
321
|
if (typeof generated === "string") {
|
|
322
322
|
const target = path.join(root, ...dbTypesPath.split("/"));
|
|
@@ -339,6 +339,6 @@ export function addTable({
|
|
|
339
339
|
}
|
|
340
340
|
return {
|
|
341
341
|
created: planned.map((file) => file.relative),
|
|
342
|
-
types: emitTypes ===
|
|
342
|
+
types: typeof emitTypes === "function" ? dbTypesPath : "skipped",
|
|
343
343
|
};
|
|
344
344
|
}
|
package/scaffold/add.mjs
CHANGED
|
@@ -26,6 +26,7 @@ import { addApp } from './add-app.mjs';
|
|
|
26
26
|
import { ensureLiteCli, selectLiteCliEntries } from './add-cli-lite.mjs';
|
|
27
27
|
import { addLiteWebhookInbox, selectLiteWebhookEntries } from './add-inbox-lite.mjs';
|
|
28
28
|
import { jsonText } from './app.mjs';
|
|
29
|
+
import { byCodeUnit } from '../report/order.mjs';
|
|
29
30
|
import { registerLiteDomainService, registerLiteExports } from './lite-exports.mjs';
|
|
30
31
|
|
|
31
32
|
const KEBAB = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
@@ -290,8 +291,8 @@ function topicsOfPatterns(patterns) {
|
|
|
290
291
|
function register({ declarationFile, spec }) {
|
|
291
292
|
const declaration = JSON.parse(fs.readFileSync(declarationFile, 'utf8'));
|
|
292
293
|
const be = declaration.sides.be;
|
|
293
|
-
const patterns = [...new Set([...(be.patterns ?? []), ...spec.patterns])].sort();
|
|
294
|
-
const kinds = [...new Set([...(be.kinds ?? []), ...(spec.trigger ? [spec.trigger] : [])])].sort();
|
|
294
|
+
const patterns = [...new Set([...(be.patterns ?? []), ...spec.patterns])].sort(byCodeUnit);
|
|
295
|
+
const kinds = [...new Set([...(be.kinds ?? []), ...(spec.trigger ? [spec.trigger] : [])])].sort(byCodeUnit);
|
|
295
296
|
be.patterns = patterns;
|
|
296
297
|
if (kinds.length) be.kinds = kinds;
|
|
297
298
|
fs.writeFileSync(declarationFile, jsonText(declaration));
|
package/scaffold/app.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import { unlinkOnly } from '../runtime/scripts/api/fs/lib.mjs';
|
|
|
25
25
|
import { revParseQuery } from '../runtime/scripts/api/git/rev-parse-query.mjs';
|
|
26
26
|
import { lsFiles } from '../runtime/scripts/api/git/ls-files.mjs';
|
|
27
27
|
import { insidePath, samePath } from '../runtime/scripts/lib/path-key.mjs';
|
|
28
|
+
import { byCodeUnit } from '../report/order.mjs';
|
|
28
29
|
|
|
29
30
|
const NAME = /^[a-z][a-z0-9-]*$/;
|
|
30
31
|
const APP_DIR = '__app__';
|
|
@@ -64,7 +65,7 @@ export const packageJsonText = value => `${JSON.stringify(value, null, 2)}\n`;
|
|
|
64
65
|
* The apps a new app starts with: on the be side the `core` api app and the `cli` app over one `primary` connection, two Next apps
|
|
65
66
|
* (landing, app) on the fe side, which read the be contracts for their codegen.
|
|
66
67
|
*/
|
|
67
|
-
|
|
68
|
+
const STARTER_SIDES = Object.freeze({
|
|
68
69
|
be: Object.freeze({ apps: [{ name: 'core', kind: 'api' }, { name: 'cli', kind: 'cli' }], kinds: ['api', 'cli'], connections: [{ name: 'primary', envPrefix: 'PRIMARY_DB', owner: 'core', isolation: 'database' }] }),
|
|
69
70
|
// fe: the landing and the product app, over the shared ui and i18n packages (both opt-in slots, enabled here).
|
|
70
71
|
fe: Object.freeze({ apps: [{ name: 'landing', kind: 'next' }, { name: 'app', kind: 'next' }], reads: ['be/contracts/'], optionalSlots: ['fe.package.ui', 'fe.package.i18n'] }),
|
|
@@ -277,6 +278,7 @@ const liteBaseFile = (scope, rel) => {
|
|
|
277
278
|
&& rel !== 'src/modules/platform/database/connection-source.client.ts'
|
|
278
279
|
&& rel !== 'src/modules/platform/database/migrate-connections.client.ts'
|
|
279
280
|
&& rel !== 'src/modules/platform/database/seed-connections.client.ts'
|
|
281
|
+
&& rel !== 'src/modules/platform/primitives/sequence.policy.ts'
|
|
280
282
|
&& !LITE_DEFERRED_GENERATOR_FILES.has(rel)
|
|
281
283
|
&& !rel.startsWith('src/tests/')
|
|
282
284
|
&& !rel.endsWith('.spec.ts');
|
|
@@ -486,7 +488,7 @@ export function scaffoldApp({ name, into, presets, edition = 'full', manifest =
|
|
|
486
488
|
let locked;
|
|
487
489
|
try { locked = lock(root); } finally { writes.lockfile(); }
|
|
488
490
|
if (!locked.ok) throw new ScaffoldError('HFS_SCAFFOLD_LOCK_FAILED', `\`${LOCK_STEP}\` could not resolve the lockfile of ${root} (${locked.detail}). Check the network and the npm registry, then run starci app scaffold ${name} again`);
|
|
489
|
-
return { root, files: [...new Set([...files.map(file => file.path), ...targets.map(target => target.path), ...(app.edition === 'lite' ? [dbTypesPath] : []), 'package-lock.json'])].sort() };
|
|
491
|
+
return { root, files: [...new Set([...files.map(file => file.path), ...targets.map(target => target.path), ...(app.edition === 'lite' ? [dbTypesPath] : []), 'package-lock.json'])].sort(byCodeUnit) };
|
|
490
492
|
} catch (error) {
|
|
491
493
|
const held = writes.rollback();
|
|
492
494
|
if (held.length) error.message += `; cleanup retained replaced or occupied entries: ${held.join(', ')}`;
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
// The edition gate of `starci app add` and `starci app new` (design 8.6 R8): a verb whose file tree lands in a slot the app's
|
|
2
|
-
// edition does not have - the slot's `editions` exclude it, or its `litePresence` is forbidden - is a full-edition
|
|
3
|
-
// capability and refuses before anything is planned or written. The slot resolver of the app answers, the same
|
|
4
|
-
// view `starci app check` reads; the noun's name is never the test, so a slot the lite manifest forbids refuses every
|
|
5
|
-
// verb that writes into it, with no noun list kept anywhere. The refusal carries no failure code by design: the
|
|
6
|
-
// scaffolders of packages/hfs keep their codes out of the runtime catalog (like HFS_SCAFFOLD_LOCK_FAILED).
|
|
7
|
-
|
|
8
|
-
/** The refusal of a verb a non-full edition does not carry: printed verbatim by the CLI, exit 2, nothing written. */
|
|
9
|
-
export class EditionRefusal extends Error {
|
|
10
|
-
constructor(command) {
|
|
11
|
-
super(`${command}: full edition only; run starci app upgrade --edition full`);
|
|
12
|
-
this.name = 'EditionRefusal';
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Refuses `command` (`add <noun>`, `new <noun>`) when one of `slotIds` - the slots the verb writes into - does not
|
|
18
|
-
* exist for the edition of `resolver`'s repository or is forbidden under it. `resolver` is the
|
|
19
|
-
* createSlotResolver() view of the app; every scope answers a slot id. Under full every verb slot exists and none is
|
|
20
|
-
* forbidden, so the gate never fires. A slot id the view does not answer (an `editions` exclusion, or an unknown id) refuses
|
|
21
|
-
* in every edition alike: the view cannot tell them apart, and the callers name only slots of the manifest.
|
|
22
|
-
*/
|
|
23
|
-
export function refuseInEdition({ resolver, slotIds, command }) {
|
|
24
|
-
if (slotIds.some((id) => resolver.slot(id) === null || resolver.slot(id).presence === 'forbidden')) throw new EditionRefusal(command);
|
|
25
|
-
}
|
|
1
|
+
// The edition gate of `starci app add` and `starci app new` (design 8.6 R8): a verb whose file tree lands in a slot the app's
|
|
2
|
+
// edition does not have - the slot's `editions` exclude it, or its `litePresence` is forbidden - is a full-edition
|
|
3
|
+
// capability and refuses before anything is planned or written. The slot resolver of the app answers, the same
|
|
4
|
+
// view `starci app check` reads; the noun's name is never the test, so a slot the lite manifest forbids refuses every
|
|
5
|
+
// verb that writes into it, with no noun list kept anywhere. The refusal carries no failure code by design: the
|
|
6
|
+
// scaffolders of packages/hfs keep their codes out of the runtime catalog (like HFS_SCAFFOLD_LOCK_FAILED).
|
|
7
|
+
|
|
8
|
+
/** The refusal of a verb a non-full edition does not carry: printed verbatim by the CLI, exit 2, nothing written. */
|
|
9
|
+
export class EditionRefusal extends Error {
|
|
10
|
+
constructor(command) {
|
|
11
|
+
super(`${command}: full edition only; run starci app upgrade --edition full`);
|
|
12
|
+
this.name = 'EditionRefusal';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Refuses `command` (`add <noun>`, `new <noun>`) when one of `slotIds` - the slots the verb writes into - does not
|
|
18
|
+
* exist for the edition of `resolver`'s repository or is forbidden under it. `resolver` is the
|
|
19
|
+
* createSlotResolver() view of the app; every scope answers a slot id. Under full every verb slot exists and none is
|
|
20
|
+
* forbidden, so the gate never fires. A slot id the view does not answer (an `editions` exclusion, or an unknown id) refuses
|
|
21
|
+
* in every edition alike: the view cannot tell them apart, and the callers name only slots of the manifest.
|
|
22
|
+
*/
|
|
23
|
+
export function refuseInEdition({ resolver, slotIds, command }) {
|
|
24
|
+
if (slotIds.some((id) => resolver.slot(id) === null || resolver.slot(id).presence === 'forbidden')) throw new EditionRefusal(command);
|
|
25
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs"
|
|
2
2
|
import path from "node:path"
|
|
3
3
|
import { ScaffoldError } from "./service.mjs"
|
|
4
|
+
import { byCodeUnit } from "../report/order.mjs"
|
|
4
5
|
|
|
5
6
|
const REGISTRATIONS = Object.freeze({
|
|
6
7
|
api: [
|
|
@@ -126,7 +127,7 @@ function addExport(text, registration) {
|
|
|
126
127
|
const line = new RegExp(`^${kind} \\{ ([^}]*) \\} from "${escape(registration.source)}"$`, "m")
|
|
127
128
|
const found = line.exec(text)
|
|
128
129
|
if (found) {
|
|
129
|
-
const names = [...new Set([...found[1].split(",").map((name) => name.trim()), ...registration.names])].sort()
|
|
130
|
+
const names = [...new Set([...found[1].split(",").map((name) => name.trim()), ...registration.names])].sort(byCodeUnit)
|
|
130
131
|
return text.replace(line, `${kind} { ${names.join(", ")} } from "${registration.source}"`)
|
|
131
132
|
}
|
|
132
133
|
const added = `${kind} { ${registration.names.join(", ")} } from "${registration.source}"`
|
package/scaffold/service.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { createRequire } from 'node:module';
|
|
|
17
17
|
import { explainPath } from '../runtime/scripts/hfs/check.mjs';
|
|
18
18
|
import { createSlotResolver, loadSlotManifest, readRepoDeclaration } from '../runtime/scripts/hfs/slots.mjs';
|
|
19
19
|
import { refuseInEdition } from './edition-gate.mjs';
|
|
20
|
+
import { byCodeUnit } from '../report/order.mjs';
|
|
20
21
|
|
|
21
22
|
export class ScaffoldError extends Error {
|
|
22
23
|
constructor(code, message) {
|
|
@@ -182,8 +183,8 @@ function specSkeleton({ shape, serviceFile, manifest = loadSlotManifest() }) {
|
|
|
182
183
|
if (kit.size) lines.push(importLine([...kit].sort((a, b) => a.localeCompare(b, 'en', { sensitivity: 'base' })), KIT));
|
|
183
184
|
const modules = [...new Set([...imports.keys(), ...typeImports.keys()])].sort(compareModules);
|
|
184
185
|
for (const module of modules) {
|
|
185
|
-
if (imports.has(module)) lines.push(importLine([...imports.get(module)].sort(), module));
|
|
186
|
-
if (typeImports.has(module)) lines.push(importLine([...typeImports.get(module)].sort(), module, true));
|
|
186
|
+
if (imports.has(module)) lines.push(importLine([...imports.get(module)].sort(byCodeUnit), module));
|
|
187
|
+
if (typeImports.has(module)) lines.push(importLine([...typeImports.get(module)].sort(byCodeUnit), module, true));
|
|
187
188
|
}
|
|
188
189
|
lines.push(`import { ${shape.className} } from "${subject}"`);
|
|
189
190
|
lines.push('');
|
|
@@ -265,8 +266,8 @@ function serviceSource({ name, dependencies = [] }) {
|
|
|
265
266
|
}
|
|
266
267
|
const lines = [];
|
|
267
268
|
for (const module of [...new Set([...values.keys(), ...types.keys()])].sort(compareModules)) {
|
|
268
|
-
if (values.has(module)) lines.push(importLine([...values.get(module)].sort(), module));
|
|
269
|
-
if (types.has(module)) lines.push(importLine([...types.get(module)].sort(), module, true));
|
|
269
|
+
if (values.has(module)) lines.push(importLine([...values.get(module)].sort(byCodeUnit), module));
|
|
270
|
+
if (types.has(module)) lines.push(importLine([...types.get(module)].sort(byCodeUnit), module, true));
|
|
270
271
|
}
|
|
271
272
|
lines.push('', '@Injectable()', `/** The ${words(name).join(' ')} service: state what it decides in one sentence. */`);
|
|
272
273
|
if (dependencies.length === 0) lines.push(`export class ${className} {}`);
|
package/sync/hygiene.mjs
CHANGED
|
@@ -7,15 +7,22 @@
|
|
|
7
7
|
// files; scripts/gates/hfs-sync.mjs judges every tracked file.
|
|
8
8
|
//
|
|
9
9
|
// Run inside a full runtime checkout — the repository under judgment is the checkout — it also reports the
|
|
10
|
-
// state-root ledger
|
|
11
|
-
// LEDGER_LEGACY_WORK_SQLITE (COOK-BRIEF F4 handover, incident 2026-09-30). Any other repository — a product repo,
|
|
10
|
+
// state-root ledger finding of that checkout's scripts/housekeeping/hk-orphan-ledgers.mjs: LEDGER_ORPHAN_STATE_ROOT. Any other repository — a product repo,
|
|
12
11
|
// a bare repo — carries no scripts/housekeeping/ledger-hygiene.mjs at its root, so that section is silently absent:
|
|
13
12
|
// never a crash, never machine-state findings blamed on a repository that does not own them.
|
|
14
|
-
import { execFileSync } from 'node:child_process';
|
|
15
13
|
import fs from 'node:fs';
|
|
16
14
|
import path from 'node:path';
|
|
17
15
|
import { pathToFileURL } from 'node:url';
|
|
18
16
|
import { secretFileFindings } from '../runtime/scripts/hfs/rules/secrets.mjs';
|
|
17
|
+
import { runGit } from '../runtime/scripts/api/git/lib.mjs';
|
|
18
|
+
|
|
19
|
+
// One git call through the runtime's git owner (scripts/api/git/lib.mjs): stdout, or the failure thrown the way execFileSync threw it (error.status carries the exit code). The caller's own env is passed on unchanged: a hook runs with GIT_INDEX_FILE set, which the staged reads must keep seeing.
|
|
20
|
+
const git = (cwd, args, options = {}) => {
|
|
21
|
+
const run = runGit(args, { cwd, env: process.env, ...options });
|
|
22
|
+
if (run.error) throw run.error;
|
|
23
|
+
if (run.status !== 0) throw Object.assign(new Error(`git ${args[0]} exited ${run.status}`), { status: run.status });
|
|
24
|
+
return run.stdout;
|
|
25
|
+
};
|
|
19
26
|
|
|
20
27
|
const PLAINTEXT_NAME = /(^|\/)(\.env(\..*)?|[^/]*\.(pem|key|identity|age))$/;
|
|
21
28
|
/** The app root's work tree and stack tree, app-relative (`starci app hygiene` runs at the app root). */
|
|
@@ -27,7 +34,7 @@ const GUARDED = file => file.startsWith(WORK) || file.startsWith(STACKS);
|
|
|
27
34
|
function ignoredAmong(cwd, files) {
|
|
28
35
|
if (files.length === 0) return new Set();
|
|
29
36
|
try {
|
|
30
|
-
const out =
|
|
37
|
+
const out = git(cwd, ['check-ignore', '--no-index', '-z', '--stdin'], { input: files.join('\0'), maxBuffer: 64 * 1024 * 1024 });
|
|
31
38
|
return new Set(out.split('\0').filter(Boolean));
|
|
32
39
|
} catch (error) {
|
|
33
40
|
if (error.status === 1) return new Set();
|
|
@@ -50,7 +57,7 @@ export function hygieneFindings(files, ignored) {
|
|
|
50
57
|
return findings;
|
|
51
58
|
}
|
|
52
59
|
|
|
53
|
-
const gitList = (cwd, args) =>
|
|
60
|
+
const gitList = (cwd, args) => git(cwd, args, { maxBuffer: 64 * 1024 * 1024 }).split('\0').filter(Boolean);
|
|
54
61
|
|
|
55
62
|
/** Files staged for the next commit (added, copied, modified, renamed). */
|
|
56
63
|
export const stagedFiles = cwd => gitList(cwd, ['diff', '--cached', '--name-only', '--diff-filter=ACMR', '-z']);
|
|
@@ -63,7 +70,7 @@ const MAX_STAGED_BYTES = 1024 * 1024;
|
|
|
63
70
|
/** The text of `file` as the index holds it (what the commit would record), or null when it is absent, binary or over 1 MB. */
|
|
64
71
|
function stagedText(cwd, file) {
|
|
65
72
|
try {
|
|
66
|
-
const blob =
|
|
73
|
+
const blob = git(cwd, ['show', `:${file}`], { encoding: 'buffer', maxBuffer: MAX_STAGED_BYTES * 2 });
|
|
67
74
|
return blob.length > MAX_STAGED_BYTES || blob.includes(0) ? null : blob.toString('utf8');
|
|
68
75
|
} catch {
|
|
69
76
|
return null;
|
|
@@ -89,7 +96,7 @@ export function judge(cwd, files) {
|
|
|
89
96
|
// bare repo — has no such file, so the section is silently absent wherever this module happens to be installed.
|
|
90
97
|
const ledgerHygieneScript = cwd => {
|
|
91
98
|
try {
|
|
92
|
-
const root =
|
|
99
|
+
const root = git(cwd, ['rev-parse', '--show-toplevel']).trim();
|
|
93
100
|
return path.join(root, 'scripts', 'housekeeping', 'ledger-hygiene.mjs');
|
|
94
101
|
} catch {
|
|
95
102
|
return null;
|
|
@@ -97,7 +104,7 @@ const ledgerHygieneScript = cwd => {
|
|
|
97
104
|
};
|
|
98
105
|
|
|
99
106
|
/**
|
|
100
|
-
* The state-root ledger findings (LEDGER_ORPHAN_STATE_ROOT
|
|
107
|
+
* The state-root ledger findings (LEDGER_ORPHAN_STATE_ROOT) of the checkout containing
|
|
101
108
|
* `cwd`, as {code, file, message} entries; [] when that checkout carries no ledger-hygiene script. Never throws: a
|
|
102
109
|
* report failure is one HFS_LEDGER_HYGIENE_UNAVAILABLE finding, not a crash of `starci app hygiene`.
|
|
103
110
|
*/
|
|
@@ -109,7 +116,6 @@ async function ledgerHygieneFindings(cwd = process.cwd()) {
|
|
|
109
116
|
const report = await ledgerHygieneReport({ apply: false });
|
|
110
117
|
return [
|
|
111
118
|
...report.orphans.map(o => ({ code: o.code, file: o.ledgerId, message: `ledger ${o.name ?? o.ledgerId} - ${o.reason}; source roots: ${o.sourceRoots.join(', ') || '(none)'}` })),
|
|
112
|
-
...report.legacy.map(l => ({ code: l.code, file: l.repoRoot, message: `${l.files.length} legacy file(s) still in the repo: ${l.files.join(', ')}` })),
|
|
113
119
|
];
|
|
114
120
|
} catch (error) {
|
|
115
121
|
return [{ code: 'HFS_LEDGER_HYGIENE_UNAVAILABLE', file: script, message: `could not run: ${String(error?.message ?? error).slice(0, 200)}` }];
|
package/sync/index.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import { managedGroupOf } from '../runtime/scripts/hfs/edition-slots.mjs';
|
|
|
25
25
|
import { feAppPackageName } from '../runtime/scripts/hfs/rules/monorepo.mjs';
|
|
26
26
|
import { DEFAULT_PORT, NODE_IMAGE, dockerfilePath } from '../runtime/scripts/hfs/rules/docker.mjs';
|
|
27
27
|
import { readDeclaredSonarKey } from './sonar-key.mjs';
|
|
28
|
+
import { byCodeUnit } from '../report/order.mjs';
|
|
28
29
|
|
|
29
30
|
export const TEMPLATES_DIR = path.join(import.meta.dirname, '..', 'templates');
|
|
30
31
|
const NODE_MAJOR = 22;
|
|
@@ -149,7 +150,7 @@ export function appSource(root) {
|
|
|
149
150
|
}
|
|
150
151
|
};
|
|
151
152
|
for (const dir of ['apps', 'src']) walk(dir);
|
|
152
|
-
return { files: files.sort(), read: file => fs.readFileSync(path.join(root, 'be', file), 'utf8') };
|
|
153
|
+
return { files: files.sort(byCodeUnit), read: file => fs.readFileSync(path.join(root, 'be', file), 'utf8') };
|
|
153
154
|
}
|
|
154
155
|
|
|
155
156
|
/**
|
|
@@ -241,10 +242,13 @@ export function imageFiles(app) {
|
|
|
241
242
|
const side = app.sides.fe;
|
|
242
243
|
const inputs = [...(opensPackages(side) ? ['fe/packages'] : []), ...((side.reads ?? []).includes('be/contracts/') ? ['be/contracts'] : [])];
|
|
243
244
|
const copyInputs = inputs.map(folder => `COPY ${folder} ${folder}\n`).join('');
|
|
245
|
+
// The folders outside be/ that the back end declares it reads (the generated Supabase types of a lite back end, which an upgraded app keeps reading) are in every one of its images.
|
|
246
|
+
const beFolders = (app.sides.be.reads ?? []).map(folder => folder.replace(/\/$/, ''));
|
|
247
|
+
const copyBeInputs = beFolders.map(folder => `COPY ${folder} ${folder}\n`).join('');
|
|
244
248
|
const entries = imageEntries(app).map(({ side: which, entry: item }) => ({ side: which, item }));
|
|
245
249
|
return entries.map(({ side: which, item }) => {
|
|
246
250
|
const template = IMAGE_TEMPLATE[item.kind];
|
|
247
|
-
const vars = { project: app.project, app: item.name, nodeImage: NODE_IMAGE, port: DEFAULT_PORT, copyInputs };
|
|
251
|
+
const vars = { project: app.project, app: item.name, nodeImage: NODE_IMAGE, port: DEFAULT_PORT, copyInputs, copyBeInputs };
|
|
248
252
|
return { path: dockerfilePath(which, item.name), content: render(readBundled(`${template}/Dockerfile`), vars) };
|
|
249
253
|
});
|
|
250
254
|
}
|
|
@@ -281,7 +285,7 @@ export function variables(app, scope, presets, sonarKey, manifest = loadSlotMani
|
|
|
281
285
|
}
|
|
282
286
|
|
|
283
287
|
// The scripts of a package.json as one comparable text: a line per script, sorted by name (order is not drift).
|
|
284
|
-
const scriptsText = scripts => `${Object.keys(scripts).sort().map(name => `${name}: ${typeof scripts[name] === 'string' ? scripts[name] : JSON.stringify(scripts[name])}`).join('\n')}\n`;
|
|
288
|
+
const scriptsText = scripts => `${Object.keys(scripts).sort(byCodeUnit).map(name => `${name}: ${typeof scripts[name] === 'string' ? scripts[name] : JSON.stringify(scripts[name])}`).join('\n')}\n`;
|
|
285
289
|
|
|
286
290
|
function parseScripts(text) {
|
|
287
291
|
try {
|
|
@@ -24,12 +24,12 @@ jobs:
|
|
|
24
24
|
env:
|
|
25
25
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
26
26
|
steps:
|
|
27
|
-
- uses: actions/checkout@v4
|
|
28
|
-
- uses: actions/setup-node@v4
|
|
27
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
28
|
+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
29
29
|
with:
|
|
30
30
|
node-version: {{nodeMajor}}
|
|
31
31
|
cache: npm
|
|
32
|
-
- run: npm ci
|
|
32
|
+
- run: npm ci --ignore-scripts
|
|
33
33
|
- name: lint
|
|
34
34
|
run: npm run lint -- --sonar reports/lint.sonar.json
|
|
35
35
|
- name: format
|
|
@@ -39,7 +39,7 @@ jobs:
|
|
|
39
39
|
- name: unit
|
|
40
40
|
run: npm test -- --ci
|
|
41
41
|
- name: coverage upload
|
|
42
|
-
uses: codecov/codecov-action@v5
|
|
42
|
+
uses: codecov/codecov-action@0fb7174895f61a3b6b78fc075e0cd60383518dac # v5.5.5
|
|
43
43
|
if: ${{ !cancelled() && startsWith(github.ref, 'refs/tags/v') }}
|
|
44
44
|
with:
|
|
45
45
|
use_oidc: true
|
|
@@ -12,11 +12,11 @@ jobs:
|
|
|
12
12
|
e2e:
|
|
13
13
|
runs-on: ubuntu-latest
|
|
14
14
|
steps:
|
|
15
|
-
- uses: actions/checkout@v4
|
|
16
|
-
- uses: actions/setup-node@v4
|
|
15
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
16
|
+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
17
17
|
with:
|
|
18
18
|
node-version: {{nodeMajor}}
|
|
19
19
|
cache: npm
|
|
20
|
-
- run: npm ci
|
|
20
|
+
- run: npm ci --ignore-scripts
|
|
21
21
|
- name: e2e
|
|
22
22
|
run: npm run test:e2e
|
|
@@ -25,9 +25,9 @@ jobs:
|
|
|
25
25
|
include:
|
|
26
26
|
{{imageMatrix}}
|
|
27
27
|
steps:
|
|
28
|
-
- uses: actions/checkout@v4
|
|
29
|
-
- uses: docker/setup-buildx-action@v3
|
|
30
|
-
- uses: docker/build-push-action@v6
|
|
28
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
29
|
+
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
|
30
|
+
- uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
|
31
31
|
with:
|
|
32
32
|
context: .
|
|
33
33
|
file: ${{ matrix.file }}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
- uses: SonarSource/sonarqube-scan-action@v7
|
|
1
|
+
- uses: SonarSource/sonarqube-scan-action@c7ee0f9df90b7aa20e8dcf9695dcfe2e7da5b4f2 # v7.2.1
|
|
2
2
|
if: ${{ !cancelled() && startsWith(github.ref, 'refs/tags/v') && env.SONAR_TOKEN != '' }}
|
|
3
3
|
env:
|
|
4
4
|
SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
|
|
5
|
-
- uses: SonarSource/sonarqube-quality-gate-action@v1
|
|
5
|
+
- uses: SonarSource/sonarqube-quality-gate-action@7a5fffe8e523c40e0c740b6bc2712ab503e52efa # v1.2.1
|
|
6
6
|
if: ${{ !cancelled() && startsWith(github.ref, 'refs/tags/v') && env.SONAR_TOKEN != '' }}
|
|
7
7
|
timeout-minutes: 10
|
|
8
8
|
env:
|
|
@@ -20,12 +20,12 @@ jobs:
|
|
|
20
20
|
env:
|
|
21
21
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
22
22
|
steps:
|
|
23
|
-
- uses: actions/checkout@v4
|
|
24
|
-
- uses: actions/setup-node@v4
|
|
23
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
24
|
+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
25
25
|
with:
|
|
26
26
|
node-version: {{nodeMajor}}
|
|
27
27
|
cache: npm
|
|
28
|
-
- run: npm ci
|
|
28
|
+
- run: npm ci --ignore-scripts
|
|
29
29
|
- name: lint
|
|
30
30
|
if: env.SONAR_TOKEN == ''
|
|
31
31
|
run: npm run lint
|
|
@@ -47,12 +47,12 @@ jobs:
|
|
|
47
47
|
db-types:
|
|
48
48
|
runs-on: ubuntu-latest
|
|
49
49
|
steps:
|
|
50
|
-
- uses: actions/checkout@v4
|
|
51
|
-
- uses: actions/setup-node@v4
|
|
50
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
51
|
+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
52
52
|
with:
|
|
53
53
|
node-version: {{nodeMajor}}
|
|
54
54
|
cache: npm
|
|
55
|
-
- run: npm ci
|
|
55
|
+
- run: npm ci --ignore-scripts
|
|
56
56
|
- name: start Supabase
|
|
57
57
|
run: npm run db:start
|
|
58
58
|
- name: apply migrations
|
|
@@ -21,11 +21,11 @@ jobs:
|
|
|
21
21
|
SUPABASE_DB_PASSWORD: ${{ secrets.SUPABASE_DB_PASSWORD }}
|
|
22
22
|
SUPABASE_PROJECT_ID: ${{ secrets.SUPABASE_PROJECT_ID }}
|
|
23
23
|
steps:
|
|
24
|
-
- uses: actions/checkout@v4
|
|
25
|
-
- uses: actions/setup-node@v4
|
|
24
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
25
|
+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
26
26
|
with:
|
|
27
27
|
node-version: {{nodeMajor}}
|
|
28
28
|
cache: npm
|
|
29
|
-
- run: npm ci
|
|
29
|
+
- run: npm ci --ignore-scripts
|
|
30
30
|
- name: push migrations
|
|
31
31
|
run: npm run db:push
|
|
@@ -25,9 +25,9 @@ jobs:
|
|
|
25
25
|
include:
|
|
26
26
|
{{imageMatrix}}
|
|
27
27
|
steps:
|
|
28
|
-
- uses: actions/checkout@v4
|
|
29
|
-
- uses: docker/setup-buildx-action@v3
|
|
30
|
-
- uses: docker/build-push-action@v6
|
|
28
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
29
|
+
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
|
30
|
+
- uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
|
31
31
|
with:
|
|
32
32
|
context: .
|
|
33
33
|
file: ${{ matrix.file }}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# StarCi .starciwork: product content only (
|
|
1
|
+
# StarCi .starciwork: product content only (work-layout.yaml shape.productPaths).
|
|
2
2
|
# Agent output (reports, checks, captures, draw rounds, UAT runs, logs, ledgers, worktrees) lives in
|
|
3
3
|
# runtime.sqlite rows and blobs outside the repository.
|
|
4
4
|
/*
|
|
@@ -19,7 +19,7 @@ RUN npm ci --ignore-scripts
|
|
|
19
19
|
|
|
20
20
|
COPY be/tsconfig.json be/tsconfig.build.json be/
|
|
21
21
|
COPY be/src be/src
|
|
22
|
-
COPY be/apps/{{app}} be/apps/{{app}}
|
|
22
|
+
{{copyBeInputs}}COPY be/apps/{{app}} be/apps/{{app}}
|
|
23
23
|
RUN npm run build:be
|
|
24
24
|
|
|
25
25
|
FROM {{nodeImage}} AS runtime
|