@starci/hfs 1.0.0 → 2.0.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 +57 -0
- package/README.md +116 -12
- package/bin/hfs.mjs +119 -15
- package/emit/compiler.mjs +35 -0
- package/emit/contracts.mjs +97 -0
- package/emit/operations-worker.mjs +24 -0
- package/emit/operations.mjs +126 -0
- package/emit/schema-worker.mjs +117 -0
- package/emit/static-graph.mjs +670 -0
- package/emit/type-schema.mjs +145 -0
- package/package.json +10 -2
- package/report/sonar.mjs +180 -0
- package/runtime/engine/admission.mjs +284 -0
- package/runtime/engine/digest.mjs +10 -0
- package/runtime/engine/ledger-db.mjs +1245 -0
- package/runtime/engine/machine-db.mjs +1484 -0
- package/runtime/engine/migrations/machine/0001-init.sql +887 -0
- package/runtime/engine/migrations/runtime/0001-init.sql +1072 -0
- package/runtime/engine/migrations/runtime/0003-usage-unavailable.sql +13 -0
- package/runtime/engine/migrations/runtime/0004-attempt-why.sql +43 -0
- package/runtime/engine/plain-object.mjs +5 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +16 -58
- package/runtime/knowledge/hfs/slots.yaml +409 -140
- package/runtime/knowledge/patterns/fe/folder.yaml +309 -0
- package/runtime/knowledge/sonar-gate.yaml +85 -0
- package/runtime/modules/kernel/failure-codes.yaml +1480 -16
- package/runtime/scripts/checks/architecture/backend.mjs +350 -0
- package/runtime/scripts/checks/architecture/background-unowned.mjs +107 -0
- package/runtime/scripts/checks/architecture/client-reaches-server.mjs +94 -0
- package/runtime/scripts/checks/architecture/clones.mjs +200 -0
- package/runtime/scripts/checks/architecture/config-unread.mjs +35 -0
- package/runtime/scripts/checks/architecture/config.mjs +310 -0
- package/runtime/scripts/checks/architecture/connection-map.mjs +208 -0
- package/runtime/scripts/checks/architecture/constructor-deps.mjs +100 -0
- package/runtime/scripts/checks/architecture/contract-fixture-guard.mjs +128 -0
- package/runtime/scripts/checks/architecture/contracts.mjs +792 -0
- package/runtime/scripts/checks/architecture/cross-app-duplicate.mjs +73 -0
- package/runtime/scripts/checks/architecture/dead-exports.mjs +265 -0
- package/runtime/scripts/checks/architecture/default-deny.mjs +129 -0
- package/runtime/scripts/checks/architecture/doc-language.mjs +39 -0
- package/runtime/scripts/checks/architecture/entrypoint.mjs +57 -0
- package/runtime/scripts/checks/architecture/error-codes.mjs +45 -0
- package/runtime/scripts/checks/architecture/error-masked.mjs +60 -0
- package/runtime/scripts/checks/architecture/fe-slot-allows.mjs +38 -0
- package/runtime/scripts/checks/architecture/feature-shape.mjs +49 -0
- package/runtime/scripts/checks/architecture/framework-pinned.mjs +83 -0
- package/runtime/scripts/checks/architecture/frontend.mjs +995 -0
- package/runtime/scripts/checks/architecture/hfs-graph.mjs +61 -0
- package/runtime/scripts/checks/architecture/hfs.mjs +521 -0
- package/runtime/scripts/checks/architecture/hooks-are-hooks.mjs +88 -0
- package/runtime/scripts/checks/architecture/i18n-keys.mjs +146 -0
- package/runtime/scripts/checks/architecture/index.mjs +316 -0
- package/runtime/scripts/checks/architecture/injection-token-exported.mjs +62 -0
- package/runtime/scripts/checks/architecture/machine-ast.mjs +138 -0
- package/runtime/scripts/checks/architecture/module-per-transport.mjs +130 -0
- package/runtime/scripts/checks/architecture/next-data.mjs +775 -0
- package/runtime/scripts/checks/architecture/owners.mjs +89 -0
- package/runtime/scripts/checks/architecture/package-shape.mjs +63 -0
- package/runtime/scripts/checks/architecture/reachability.mjs +233 -0
- package/runtime/scripts/checks/architecture/register-once.mjs +141 -0
- package/runtime/scripts/checks/architecture/registration.mjs +319 -0
- package/runtime/scripts/checks/architecture/required-files.mjs +172 -0
- package/runtime/scripts/checks/architecture/route-files-thin.mjs +97 -0
- package/runtime/scripts/checks/architecture/schema-owner.mjs +261 -0
- package/runtime/scripts/checks/architecture/size-growth.mjs +73 -0
- package/runtime/scripts/checks/architecture/source-names.mjs +607 -0
- package/runtime/scripts/checks/architecture/sql-owner.mjs +142 -0
- package/runtime/scripts/checks/architecture/sql-tokens.mjs +327 -0
- package/runtime/scripts/checks/architecture/symbols.mjs +193 -0
- package/runtime/scripts/checks/architecture/test-world-files.mjs +163 -0
- package/runtime/scripts/checks/architecture/tiers.mjs +130 -0
- package/runtime/scripts/checks/architecture/transport-owner.mjs +112 -0
- package/runtime/scripts/checks/architecture/typescript.mjs +500 -0
- package/runtime/scripts/checks/architecture/unit-spec-providers.mjs +122 -0
- package/runtime/scripts/checks/architecture.mjs +41 -0
- package/runtime/scripts/checks/common.mjs +37 -0
- package/runtime/scripts/checks/typescript-programs.mjs +82 -0
- package/runtime/scripts/lib/artifact-hold.mjs +89 -0
- package/runtime/scripts/lib/artifact-store.mjs +103 -0
- package/runtime/scripts/lib/fs-kind.mjs +10 -0
- package/runtime/scripts/lib/git.mjs +53 -0
- package/runtime/scripts/lib/hfs-allows.mjs +57 -0
- package/runtime/scripts/lib/hfs-check.mjs +254 -28
- package/runtime/scripts/lib/hfs-rules/contract.mjs +126 -0
- package/runtime/scripts/lib/hfs-rules/deps.mjs +63 -0
- package/runtime/scripts/lib/hfs-rules/fe-no-tests.mjs +47 -0
- package/runtime/scripts/lib/hfs-rules/frontend.mjs +124 -0
- package/runtime/scripts/lib/hfs-rules/lint-suppression.mjs +34 -0
- package/runtime/scripts/lib/hfs-rules/pipeline.mjs +51 -0
- package/runtime/scripts/lib/hfs-rules/proof-commands.mjs +64 -0
- package/runtime/scripts/lib/hfs-rules/read.mjs +28 -0
- package/runtime/scripts/lib/hfs-rules/repo-local-checks.mjs +32 -0
- package/runtime/scripts/lib/hfs-rules/secrets.mjs +54 -0
- package/runtime/scripts/lib/hfs-rules/spec-placement.mjs +31 -0
- package/runtime/scripts/lib/hfs-rules/stacks.mjs +54 -0
- package/runtime/scripts/lib/hfs-rules/test-topology.mjs +31 -0
- package/runtime/scripts/lib/hfs-slots.mjs +95 -41
- package/runtime/scripts/lib/hfs-tree.mjs +80 -0
- package/runtime/scripts/lib/hfs-view.mjs +68 -0
- package/runtime/scripts/lib/json.mjs +22 -0
- package/runtime/scripts/lib/language.mjs +107 -0
- package/runtime/scripts/lib/path-key.mjs +2 -0
- package/runtime/scripts/lib/redact.mjs +148 -0
- package/runtime/scripts/lib/repo-identity.mjs +50 -0
- package/runtime/scripts/lib/safe-remove.mjs +179 -0
- package/runtime/scripts/lib/secret-patterns.mjs +44 -0
- package/runtime/scripts/lib/sleep-sync.mjs +17 -0
- package/runtime/scripts/lib/stack-declaration.mjs +52 -0
- package/runtime/scripts/lib/stack-services.mjs +217 -0
- package/runtime/scripts/lib/test-secrets.mjs +120 -0
- package/scaffold/service.mjs +333 -0
- package/sync/format.mjs +46 -0
- package/sync/hygiene.mjs +56 -24
- package/sync/index.mjs +126 -41
- package/sync/managed.mjs +170 -0
- package/sync/skeleton.mjs +32 -10
- package/sync/sonar-key.mjs +13 -0
- package/sync/ts-strict.mjs +48 -0
- package/templates/{common → be/ci-workflows/github/workflows}/ci.yml +13 -13
- package/templates/be/{e2e.yml → ci-workflows/github/workflows/e2e.yml} +1 -0
- package/templates/be/hooks/husky/pre-commit +13 -0
- package/templates/be/hooks/husky/pre-push +7 -0
- package/templates/be/package-scripts/package.json +21 -0
- package/templates/be/{sonar-project.properties → quality-config/sonar-project.properties} +2 -3
- package/templates/be/skeleton/apps/__app__/src/app.module.ts +14 -4
- package/templates/be/skeleton/apps/__app__/src/main.ts +9 -6
- package/templates/be/skeleton/scripts/.gitkeep +0 -0
- package/templates/be/skeleton/src/features/system-health/application/check-liveness.contracts.ts +2 -0
- package/templates/be/skeleton/src/features/system-health/application/check-liveness.handler.ts +22 -0
- package/templates/be/skeleton/src/features/system-health/application/check-liveness.query.ts +11 -0
- package/templates/be/skeleton/src/features/system-health/index.ts +1 -1
- package/templates/be/skeleton/src/features/system-health/system-health.module.ts +3 -3
- package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.ts +8 -9
- package/templates/be/skeleton/src/features/system-health/transport/http/system-health-http.module.ts +3 -6
- package/templates/be/skeleton/src/modules/domain/liveness/index.ts +3 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.contracts.ts +9 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.module.ts +14 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.options.ts +2 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.service.spec.ts +40 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.service.ts +26 -0
- package/templates/be/skeleton/src/modules/platform/clock/clock.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/clock/clock.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/clock/clock.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/clock/clock.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/clock/clock.port.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/clock/index.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/clock/system-clock.service.spec.ts +20 -0
- package/templates/be/skeleton/src/modules/platform/clock/system-clock.service.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/composition/composition.decorators.ts +8 -0
- package/templates/be/skeleton/src/modules/platform/composition/index.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/config/config.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/config/index.ts +2 -3
- package/templates/be/skeleton/src/modules/platform/config/server.config.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/config/server.options.ts +0 -3
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.contracts.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.handler.ts +27 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.log-events.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module.ts +18 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/index.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/errors/error.filter.ts +8 -12
- package/templates/be/skeleton/src/modules/platform/errors/errors.log-events.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/errors/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/logging/index.ts +4 -4
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +99 -0
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +38 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +23 -10
- package/templates/be/skeleton/src/modules/platform/logging/logging.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.port.ts +15 -0
- package/templates/be/tool-config/eslint.config.mjs +3 -0
- package/templates/be/tool-config/jest.config.js +1 -0
- package/templates/be/tool-config/prettierignore +8 -0
- package/templates/be/tool-config/prettierrc +1 -0
- package/templates/be/tool-config/src/tests/tsconfig.json +5 -0
- package/templates/be/tool-config/tsconfig.build.json +5 -0
- package/templates/be/tool-config/tsconfig.json +11 -0
- package/templates/common/gitignore.base +1 -1
- package/templates/fe/ci-workflows/github/workflows/ci.yml +54 -0
- package/templates/fe/hooks/husky/pre-commit +16 -0
- package/templates/fe/hooks/husky/pre-push +6 -0
- package/templates/fe/package-scripts/package.json +17 -0
- package/templates/fe/parts/api-client.ts +44 -0
- package/templates/fe/parts/api-outcome.ts +7 -0
- package/templates/fe/quality-config/sonar-project.properties +8 -0
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/layout.tsx +1 -1
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/not-found.tsx +1 -1
- package/templates/fe/skeleton/apps/__app__/src/app/global-error.tsx +1 -1
- package/templates/fe/skeleton/scripts/.gitkeep +0 -0
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/client.ts +1 -0
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/index.ts +3 -0
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/outcome.ts +1 -0
- package/templates/fe/skeleton-app/apps/__app__/src/modules/i18n/index.ts +4 -0
- package/templates/fe/skeleton-shared/apps/__app__/next.config.ts +12 -0
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/api/index.ts +2 -0
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/index.ts +9 -0
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/request.ts +5 -0
- package/templates/fe/skeleton-shared/apps/__app__/src/proxy.ts +5 -0
- package/templates/fe/skeleton-shared/packages/__family__-api/package.json +12 -0
- package/templates/fe/skeleton-shared/packages/__family__-api/src/client.ts +1 -0
- package/templates/fe/skeleton-shared/packages/__family__-api/src/index.ts +3 -0
- package/templates/fe/skeleton-shared/packages/__family__-api/src/outcome.ts +1 -0
- package/templates/fe/skeleton-shared/packages/__family__-api/tsconfig.json +5 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/package.json +18 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/app.ts +19 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/index.ts +2 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/proxy.ts +12 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/request.ts +15 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/tsconfig.json +5 -0
- package/templates/fe/tool-config/eslint.config.mjs +3 -0
- package/templates/fe/tool-config/prettierignore +10 -0
- package/templates/fe/tool-config/prettierrc +1 -0
- package/templates/fe/tool-config/stylelint.config.mjs +3 -0
- package/templates/fe/tool-config/tsconfig.json +4 -0
- package/templates/be/pre-commit +0 -8
- package/templates/be/skeleton/apps/__app__/src/__app__.composition.spec.ts +0 -31
- package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.spec.ts +0 -13
- package/templates/be/skeleton/src/modules/platform/config/env-source.spec.ts +0 -36
- package/templates/be/skeleton/src/modules/platform/config/server.config.spec.ts +0 -19
- package/templates/be/skeleton/src/modules/platform/errors/domain-error.spec.ts +0 -15
- package/templates/be/skeleton/src/modules/platform/errors/error.filter.spec.ts +0 -38
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.spec.ts +0 -33
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.ts +0 -35
- package/templates/be/skeleton/src/modules/platform/logging/log-id.ts +0 -9
- package/templates/be/skeleton/src/modules/platform/logging/logger.port.ts +0 -19
- package/templates/common/codecov.yml +0 -13
- package/templates/common/pre-push +0 -5
- package/templates/fe/e2e.yml +0 -22
- package/templates/fe/pre-commit +0 -7
- package/templates/fe/skeleton/apps/__app__/src/app/health/live/route.spec.ts +0 -10
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages.spec.ts +0 -27
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/routing.spec.ts +0 -10
- package/templates/fe/sonar-project.properties +0 -11
- /package/templates/be/skeleton/src/modules/platform/config/{env-source.ts → env-source.config.ts} +0 -0
- /package/templates/be/skeleton/src/modules/platform/errors/{domain-error.ts → domain.error.ts} +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/next.config.ts +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/config.ts +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/navigation.ts +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/request.ts +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/routing.ts +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/proxy.ts +0 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {fileURLToPath} from 'node:url';
|
|
4
|
+
|
|
5
|
+
/** Whether `target` is `root` itself or inside it (filesystem paths, either separator). */
|
|
6
|
+
export function isInside(root, target) {
|
|
7
|
+
const relative = path.relative(root, target);
|
|
8
|
+
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** True when this module is the process entry point (`node file.mjs`, not an import). */
|
|
12
|
+
export const isMain = (metaUrl, argv = process.argv) => Boolean(argv[1]) && path.resolve(argv[1]) === fileURLToPath(metaUrl);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Every entry under `dir` that is not a directory, depth-first; `filter` sees the entry name and full
|
|
16
|
+
* path. `sorted` sorts each directory's entries by name (default: readdir order). `exclude`
|
|
17
|
+
* applies to files and directories before descent; `maxDepth` counts from the starting dir.
|
|
18
|
+
*/
|
|
19
|
+
export function walkFiles(dir, {filter = () => true, sorted = false, exclude = () => false,
|
|
20
|
+
maxDepth = Infinity, ignoreReadErrors = false} = {}) {
|
|
21
|
+
const out = [];
|
|
22
|
+
const visit = (current, depth) => {
|
|
23
|
+
if (depth > maxDepth) return;
|
|
24
|
+
let entries;
|
|
25
|
+
try { entries = fs.readdirSync(current, {withFileTypes: true}); }
|
|
26
|
+
catch (error) { if (ignoreReadErrors) return; throw error; }
|
|
27
|
+
if (sorted) entries = entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
const full = path.join(current, entry.name);
|
|
30
|
+
if (exclude(entry.name, full, entry)) continue;
|
|
31
|
+
if (entry.isDirectory()) visit(full, depth + 1);
|
|
32
|
+
else if (filter(entry.name, full)) out.push(full);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
visit(dir, 0);
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
// One TypeScript program per (compiler, root names, compiler options, project references) inside a program run, and
|
|
6
|
+
// one value per (kind, compiler, input) for what callers derive from those programs (the architecture context).
|
|
7
|
+
// check-scoped-lint opens one run around its architecture check and its script checkers and releases it before
|
|
8
|
+
// ESLint and the base measurement, so nothing built here outlives the run that built it. Outside a run every call
|
|
9
|
+
// builds afresh.
|
|
10
|
+
const runs = new AsyncLocalStorage();
|
|
11
|
+
|
|
12
|
+
/** A program run: run(fn) shares what is built inside fn; release() drops it. */
|
|
13
|
+
export function typeScriptProgramRun() {
|
|
14
|
+
const values = new Map();
|
|
15
|
+
return {
|
|
16
|
+
run: fn => runs.run(values, fn),
|
|
17
|
+
release: () => values.clear(),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function keyOf(value) {
|
|
22
|
+
if (value === undefined) return 'u';
|
|
23
|
+
if (value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return JSON.stringify(value);
|
|
24
|
+
if (Array.isArray(value)) {
|
|
25
|
+
const items = value.map(keyOf);
|
|
26
|
+
return items.includes(null) ? null : `[${items.join(',')}]`;
|
|
27
|
+
}
|
|
28
|
+
if (typeof value !== 'object' || (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)) return null;
|
|
29
|
+
const entries = [];
|
|
30
|
+
for (const name of Object.keys(value).sort()) {
|
|
31
|
+
const item = keyOf(value[name]);
|
|
32
|
+
if (item === null) return null;
|
|
33
|
+
entries.push(`${JSON.stringify(name)}:${item}`);
|
|
34
|
+
}
|
|
35
|
+
return `{${entries.join(',')}}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* build(), shared inside a program run by every caller that asks for the same kind, compiler and input. An input that
|
|
40
|
+
* is not plain data (a function, a class instance) is never shared.
|
|
41
|
+
*/
|
|
42
|
+
export function sharedInProgramRun(kind, ts, input, build) {
|
|
43
|
+
const values = runs.getStore();
|
|
44
|
+
const key = values ? keyOf(input) : null;
|
|
45
|
+
if (key === null) return build();
|
|
46
|
+
let byKey = values.get(ts);
|
|
47
|
+
if (!byKey) values.set(ts, byKey = new Map());
|
|
48
|
+
const id = `${kind}\0${key}`;
|
|
49
|
+
if (!byKey.has(id)) byKey.set(id, build());
|
|
50
|
+
return byKey.get(id);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** ts.createProgram, shared inside a program run. */
|
|
54
|
+
export function createTypeScriptProgram(ts, { rootNames, options, projectReferences }) {
|
|
55
|
+
return sharedInProgramRun('program', ts, { rootNames, options, projectReferences },
|
|
56
|
+
() => ts.createProgram({ rootNames, options, projectReferences }));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Read and expand a tsconfig with the caller's TypeScript compiler and optional tracked file reader. */
|
|
60
|
+
export function readTypeScriptProject(ts, configFile, readFile = ts.sys.readFile) {
|
|
61
|
+
const read = ts.readConfigFile(configFile, readFile);
|
|
62
|
+
if (read.error) return { read, parsed: null };
|
|
63
|
+
const host = readFile === ts.sys.readFile ? ts.sys : { ...ts.sys, readFile };
|
|
64
|
+
return { read, parsed: ts.parseJsonConfigFileContent(read.config, host, path.dirname(configFile), undefined, configFile) };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Resolve a project reference with the compiler's API, including older compiler fallback. */
|
|
68
|
+
export function typeScriptProjectReferencePath(ts, reference) {
|
|
69
|
+
if (typeof ts.resolveProjectReferencePath === 'function') return ts.resolveProjectReferencePath(reference);
|
|
70
|
+
return path.extname(reference.path) ? reference.path : path.join(reference.path, 'tsconfig.json');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Resolve an import using the selected project's compiler options. */
|
|
74
|
+
export function resolveTypeScriptModule(ts, specifier, from, options, host = ts.sys) {
|
|
75
|
+
return ts.resolveModuleName(specifier, from, options, host).resolvedModule?.resolvedFileName;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Configuration imports prefer Node's package resolution, then the selected TypeScript project. */
|
|
79
|
+
export function resolveNodeOrTypeScriptModule(ts, specifier, from, options, host = ts.sys) {
|
|
80
|
+
try { return createRequire(from).resolve(specifier); }
|
|
81
|
+
catch { return resolveTypeScriptModule(ts, specifier, from, options, host); }
|
|
82
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// artifact-hold.mjs — the retention exemption for job proofs. A path an indexed job artifact lives at
|
|
2
|
+
// (job_artifacts, engine/migrations/runtime/0001-init.sql), a tree holding one, and the evidence directory around one (never a shared root:
|
|
3
|
+
// the repository, .starciwork, .starciwork/evidence or .starciwork/kernel-evidence itself) are never
|
|
4
|
+
// removed by housekeeping, whatever the workflow's phase (running, finished, archived); nor is the repository's
|
|
5
|
+
// ledger .starciwork/runtime.sqlite (it holds the typed logs since 2026-09-27, scripts/kernel/typed-logs.mjs), nor the
|
|
6
|
+
// retired logs file .starciwork/logs.sqlite or its logs.sqlite.migrated-<date> copy, nor a tree holding one. Every
|
|
7
|
+
// runtime delete of a tree (safe-remove.mjs safeRemoveTree) and every housekeeping unlink asks artifactHoldOf first.
|
|
8
|
+
// Proofs, artifacts and logs are deleted ONLY by the owner-approved workflow purge (scripts/work/purge-workflow.mjs:
|
|
9
|
+
// archive to a verified ZIP, then delete the finished workflow as a unit), never here.
|
|
10
|
+
//
|
|
11
|
+
// The ledgers are the ones the machine registry enrols (machine.sqlite `ledgers`); a ledger's repository is
|
|
12
|
+
// the directory above its .starciwork. Only a target that overlaps a registered repository opens that
|
|
13
|
+
// ledger, read-only, so a temp or lane sweep never touches sqlite past the registry read.
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
17
|
+
import { hasLedgerColumn, hasLedgerTable, openLedgerReader } from '../../engine/ledger-db.mjs';
|
|
18
|
+
import { machineFileFor, readMachine } from '../../engine/machine-db.mjs';
|
|
19
|
+
import { pathKey, sameOrUnder } from './path-key.mjs';
|
|
20
|
+
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const REGISTRY_TTL_MS = 30000;
|
|
23
|
+
const norm = pathKey;
|
|
24
|
+
let cache = { file: null, at: 0, repos: [] };
|
|
25
|
+
|
|
26
|
+
const openReadOnly = (file) => openLedgerReader(file);
|
|
27
|
+
|
|
28
|
+
/** The repositories whose ledgers the registry enrols: [{repo, ledger}], cached briefly per registry file; null when the registry cannot be read. */
|
|
29
|
+
export function registeredRepos({ env = process.env, now = Date.now() } = {}) {
|
|
30
|
+
const file = machineFileFor(env);
|
|
31
|
+
if (cache.file === file && now - cache.at < REGISTRY_TTL_MS) return cache.repos;
|
|
32
|
+
const repos = fs.existsSync(file) ? readMachine((m) => m.listLedgers().map((l) => ({ ledger: l.file, repo: l.repoRoot })), null, { file, env }) : [];
|
|
33
|
+
cache = { file, at: now, repos };
|
|
34
|
+
return repos;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Null when removing `target` touches no indexed artifact; else {ledger, repo, paths:[...up to 5], count}.
|
|
39
|
+
* Held: an indexed file at or sameOrUnder `target`, or `target` inside the directory of an indexed file.
|
|
40
|
+
* A ledger that cannot be read holds its whole repository, and an unreadable registry holds every path in or
|
|
41
|
+
* holding a .starciwork directory - where every indexed artifact lives (fail closed).
|
|
42
|
+
*/
|
|
43
|
+
export function artifactHoldOf(target, { env = process.env, repos = registeredRepos({ env }) } = {}) {
|
|
44
|
+
const t = norm(target);
|
|
45
|
+
if (repos === null) {
|
|
46
|
+
const work = t.split('/').includes('.starciwork') || fs.existsSync(path.join(target, '.starciwork'));
|
|
47
|
+
return work ? { ledger: machineFileFor(env), repo: null, paths: [], count: null, error: 'the machine registry cannot be read' } : null;
|
|
48
|
+
}
|
|
49
|
+
for (const { ledger, repo } of repos) {
|
|
50
|
+
const r = norm(repo);
|
|
51
|
+
if (!sameOrUnder(t, r) && !sameOrUnder(r, t)) continue;
|
|
52
|
+
// The repository's typed logs (scripts/kernel/typed-logs.mjs: the ledger's logs table, and the retired logs.sqlite
|
|
53
|
+
// with its migrated copy) are append-only history: never swept.
|
|
54
|
+
const work = path.join(repo, '.starciwork');
|
|
55
|
+
const logFiles = [path.basename(ledger), 'logs.sqlite', ...(fs.existsSync(work) && sameOrUnder(norm(work), t) ? fs.readdirSync(work).filter((n) => /^logs\.sqlite\.migrated-/.test(n)) : [])]
|
|
56
|
+
.filter((name, i, all) => all.indexOf(name) === i).map((name) => path.join(work, name)).filter((file) => sameOrUnder(norm(file), t) && fs.existsSync(file));
|
|
57
|
+
if (logFiles.length) return { ledger, repo, paths: logFiles.map((file) => `.starciwork/${path.basename(file)}`), count: logFiles.length };
|
|
58
|
+
if (!fs.existsSync(ledger)) continue;
|
|
59
|
+
let paths;
|
|
60
|
+
try {
|
|
61
|
+
const db = openReadOnly(ledger);
|
|
62
|
+
// alpha.3: job_artifacts index blobs by sha256 and hold no repository path (no `path` column), so an alpha.3
|
|
63
|
+
// ledger holds no tree; only a ledger that still indexes paths does.
|
|
64
|
+
try { paths = hasLedgerTable(db, 'job_artifacts') && hasLedgerColumn(db, 'job_artifacts', 'path') ? db.prepare('SELECT DISTINCT path FROM job_artifacts').all().map((row) => row.path) : []; }
|
|
65
|
+
finally { db.close(); }
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return { ledger, repo, paths: [], count: null, error: String(error?.message ?? error) };
|
|
68
|
+
}
|
|
69
|
+
// "Around" holds an evidence directory, never a shared root: an artifact filed at the repository root or directly
|
|
70
|
+
// in .starciwork (e.g. a scope op indexing .starciwork/index.yaml) held every tree sameOrUnder it, so no purge could
|
|
71
|
+
// remove an archived workflow's kernel-evidence directory.
|
|
72
|
+
const shared = new Set([r, norm(work), norm(path.join(work, 'evidence')), norm(path.join(work, 'kernel-evidence'))]);
|
|
73
|
+
const held = paths.filter((rel) => {
|
|
74
|
+
const abs = norm(path.join(repo, rel));
|
|
75
|
+
const dir = norm(path.dirname(abs));
|
|
76
|
+
return sameOrUnder(abs, t) || (!shared.has(dir) && sameOrUnder(t, dir));
|
|
77
|
+
});
|
|
78
|
+
if (held.length) return { ledger, repo, paths: held.slice(0, 5), count: held.length };
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** artifactHoldOf as one refusal line, or null. */
|
|
84
|
+
export const artifactHoldReason = (target, options) => {
|
|
85
|
+
const hold = artifactHoldOf(target, options);
|
|
86
|
+
if (!hold) return null;
|
|
87
|
+
return hold.error ? `the ledger ${hold.ledger} holding indexed job artifacts cannot be read (${hold.error})`
|
|
88
|
+
: `${hold.count} indexed job artifact(s) of ${hold.ledger} (${hold.paths.join(', ')})`;
|
|
89
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Content-addressed operational evidence. Blob bytes are immutable and never removed here.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
|
|
7
|
+
export const ARTIFACT_ROOT_ENV = 'STARCI_ARTIFACT_ROOT';
|
|
8
|
+
export const artifactRoot = (env = process.env) => path.resolve(env[ARTIFACT_ROOT_ENV] || path.join(os.homedir(), '.starci', 'artifacts'));
|
|
9
|
+
const SHA = /^[a-f0-9]{64}$/;
|
|
10
|
+
const assertSha = sha => {
|
|
11
|
+
if (typeof sha !== 'string' || !SHA.test(sha)) throw new TypeError('blob sha must be a lowercase sha256 hex digest');
|
|
12
|
+
return sha;
|
|
13
|
+
};
|
|
14
|
+
const location = sha => path.join(artifactRoot(), assertSha(sha).slice(0, 2), sha);
|
|
15
|
+
const metadataPath = sha => `${location(sha)}.json`;
|
|
16
|
+
const digest = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
17
|
+
|
|
18
|
+
// Reject roots inside a checkout, including worktrees whose .git is a file.
|
|
19
|
+
function ensureExternalRoot(root) {
|
|
20
|
+
let cursor = root;
|
|
21
|
+
while (true) {
|
|
22
|
+
if (fs.existsSync(path.join(cursor, '.git'))) throw new Error(`artifact root is inside a git checkout: ${root}`);
|
|
23
|
+
const parent = path.dirname(cursor);
|
|
24
|
+
if (parent === cursor) return;
|
|
25
|
+
cursor = parent;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sourceBytes(bufferOrPath) {
|
|
30
|
+
if (Buffer.isBuffer(bufferOrPath)) return bufferOrPath;
|
|
31
|
+
if (bufferOrPath instanceof Uint8Array) return Buffer.from(bufferOrPath);
|
|
32
|
+
if (typeof bufferOrPath === 'string') return fs.readFileSync(bufferOrPath);
|
|
33
|
+
throw new TypeError('putBlob expects a Buffer, Uint8Array, or file path');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function publishOnce(destination, bytes) {
|
|
37
|
+
const dir = path.dirname(destination);
|
|
38
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
39
|
+
const temp = path.join(dir, `.${path.basename(destination)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
40
|
+
try {
|
|
41
|
+
fs.writeFileSync(temp, bytes, { flag: 'wx' });
|
|
42
|
+
try { fs.linkSync(temp, destination); }
|
|
43
|
+
catch (error) { if (error.code !== 'EEXIST') throw error; }
|
|
44
|
+
} finally { try { fs.unlinkSync(temp); } catch (error) { if (error.code !== 'ENOENT') throw error; } }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Store original bytes under their sha256. A concurrent or repeated put never replaces published bytes. */
|
|
48
|
+
export function putBlob(bufferOrPath, { mediaType = 'application/octet-stream' } = {}) {
|
|
49
|
+
if (typeof mediaType !== 'string' || !mediaType.trim()) throw new TypeError('mediaType must be a nonempty string');
|
|
50
|
+
const root = artifactRoot();
|
|
51
|
+
ensureExternalRoot(root);
|
|
52
|
+
const bytes = sourceBytes(bufferOrPath);
|
|
53
|
+
const sha = digest(bytes);
|
|
54
|
+
const destination = location(sha);
|
|
55
|
+
const metadata = { size: bytes.length, mediaType, createdAt: new Date().toISOString() };
|
|
56
|
+
// Metadata is published first so a visible blob always has a sidecar. A crash can leave an orphan sidecar;
|
|
57
|
+
// the next put completes it. The first published media type and creation time win.
|
|
58
|
+
publishOnce(metadataPath(sha), Buffer.from(JSON.stringify(metadata)));
|
|
59
|
+
publishOnce(destination, bytes);
|
|
60
|
+
const stored = fs.statSync(destination);
|
|
61
|
+
if (stored.size !== bytes.length) throw new Error(`blob size mismatch: ${sha}`);
|
|
62
|
+
const info = statBlob(sha);
|
|
63
|
+
return { sha, size: info.size, mediaType: info.mediaType };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function blobPath(sha) {
|
|
67
|
+
const file = location(sha);
|
|
68
|
+
return fs.existsSync(file) && fs.statSync(file).isFile() ? file : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const hasBlob = sha => blobPath(sha) !== null;
|
|
72
|
+
|
|
73
|
+
export function getBlob(sha) {
|
|
74
|
+
const file = blobPath(sha);
|
|
75
|
+
if (!file) throw Object.assign(new Error(`blob not found: ${sha}`), { code: 'ENOENT' });
|
|
76
|
+
const bytes = fs.readFileSync(file);
|
|
77
|
+
if (digest(bytes) !== sha) throw new Error(`blob hash mismatch: ${sha}`);
|
|
78
|
+
return bytes;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function statBlob(sha) {
|
|
82
|
+
const file = blobPath(sha);
|
|
83
|
+
if (!file) return null;
|
|
84
|
+
const { size } = fs.statSync(file);
|
|
85
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath(sha), 'utf8'));
|
|
86
|
+
if (metadata.size !== size) throw new Error(`blob sidecar size mismatch: ${sha}`);
|
|
87
|
+
return { sha, size, mediaType: metadata.mediaType, createdAt: metadata.createdAt };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function canonical(value) {
|
|
91
|
+
if (value && typeof value === 'object') {
|
|
92
|
+
if (typeof value.toJSON === 'function') return canonical(value.toJSON());
|
|
93
|
+
if (Array.isArray(value)) return value.map(item => item === undefined ? null : canonical(item));
|
|
94
|
+
return Object.fromEntries(Object.keys(value).sort().filter(key => value[key] !== undefined).map(key => [key, canonical(value[key])]));
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function putJson(obj) {
|
|
100
|
+
const encoded = JSON.stringify(canonical(obj));
|
|
101
|
+
if (encoded === undefined) throw new TypeError('putJson expects a JSON value');
|
|
102
|
+
return putBlob(Buffer.from(encoded), { mediaType: 'application/json' }).sha;
|
|
103
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// fs-kind.mjs — the forgiving fs.statSync probes a dozen scripts re-declared by hand.
|
|
2
|
+
// A missing or unreadable path is false/null, never a throw; the stat syscall decides kind.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
|
|
5
|
+
/** `p` names a file; false on any stat error (missing path, dangling link, EPERM). */
|
|
6
|
+
export const isFile = (p) => { try { return fs.statSync(p).isFile(); } catch { return false; } };
|
|
7
|
+
/** `p` names a directory; false on any stat error. */
|
|
8
|
+
export const isDir = (p) => { try { return fs.statSync(p).isDirectory(); } catch { return false; } };
|
|
9
|
+
/** `p`'s canonical spelling via the filesystem (links resolved), or null when it cannot be read. */
|
|
10
|
+
export const realpathOr = (p) => { try { return fs.realpathSync.native(p); } catch { return null; } };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// git.mjs — the one spawn the runtime's guard/kernel scripts run git through.
|
|
2
|
+
//
|
|
3
|
+
// Every copy spelt the same options by hand - encoding:'utf8', windowsHide:true, sometimes a
|
|
4
|
+
// timeout - with two shapes: `git args` in a cwd (shim's pathspec scans, footprint-scan,
|
|
5
|
+
// safe-remove's prune) and `git -C dir args` (settle-landed, install, terminal-dedupe's worktree
|
|
6
|
+
// list). gitOutput is the throwing shape (stdout text, or an Error on a non-zero exit). gitSpawn keeps the spawnSync(file, args, options) signature so an injected runner or a
|
|
7
|
+
// spec's fake takes the same three arguments; runGit is the `-C` convenience; gitResult folds the
|
|
8
|
+
// result into settle-landed's {ok, stdout, error} envelope.
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Spawn `file` (the git binary) once with `args`: utf8 text, a hidden window, never a shell.
|
|
13
|
+
* Options pass through last, so cwd, timeout, input, env or maxBuffer land as given. encoding:'buffer'
|
|
14
|
+
* is a request for raw bytes, not an encoding: spawnSync encodes a string `input` with it and throws
|
|
15
|
+
* ERR_UNKNOWN_ENCODING, so it reaches spawnSync as encoding:null (Buffer output either way).
|
|
16
|
+
*/
|
|
17
|
+
export const gitSpawn = (file, args, options = {}) => {
|
|
18
|
+
const spawn = { encoding: 'utf8', windowsHide: true, ...options };
|
|
19
|
+
if (spawn.encoding === 'buffer') spawn.encoding = null;
|
|
20
|
+
return spawnSync(file, args, spawn);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** `git args` in `cwd`, or `git -C dir args` when `dir` is given. `git` overrides the binary. */
|
|
24
|
+
export const runGit = (args, { cwd = null, dir = null, git = 'git', ...options } = {}) =>
|
|
25
|
+
gitSpawn(git, [...(dir ? ['-C', dir] : []), ...args], cwd ? { cwd, ...options } : options);
|
|
26
|
+
|
|
27
|
+
/** runGit's stdout as text; throws the spawn error, or an Error carrying git's stderr and exit status, unless git exits 0. */
|
|
28
|
+
export function gitOutput(args, options = {}) {
|
|
29
|
+
const r = runGit(args, options);
|
|
30
|
+
if (r.error) throw r.error;
|
|
31
|
+
if (r.status !== 0) throw Object.assign(new Error(`git ${args.join(' ')} exited ${r.status}: ${(r.stderr ?? '').trim()}`), { status: r.status, stderr: r.stderr });
|
|
32
|
+
return r.stdout;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** runGit's result as {ok, stdout, error}: ok only on a clean exit, error from stderr or the spawn. */
|
|
36
|
+
export function gitResult(args, options = {}) {
|
|
37
|
+
const r = runGit(args, options);
|
|
38
|
+
return {
|
|
39
|
+
ok: !r.error && r.status === 0,
|
|
40
|
+
stdout: r.stdout ?? '',
|
|
41
|
+
error: (r.stderr ?? '').trim() || String(r.error?.message ?? (r.status == null ? 'timed out' : `exit ${r.status}`)),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* git's C-quoted diff path decoded - `"b\303\251"` a `+++ ` header line prints when core.quotePath
|
|
47
|
+
* covers the name. Octal escapes become \u00XX for JSON.parse (which answers \n, \t, \" and \\);
|
|
48
|
+
* a value not wrapped in quotes passes through unchanged.
|
|
49
|
+
*/
|
|
50
|
+
export const unquoteDiffPath = (value) =>
|
|
51
|
+
/^".*"$/.test(value)
|
|
52
|
+
? JSON.parse(value.replace(/\\([0-7]{3})/g, (_, octal) => `\\u00${Number.parseInt(octal, 8).toString(16).padStart(2, '0')}`))
|
|
53
|
+
: value;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { braceVariants, globExpression } from './glob.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What a slot's `requires`, `allows` and `forbids` entries (knowledge/hfs/slots.yaml) say about a file, read through the
|
|
6
|
+
* resolver of scripts/lib/hfs-slots.mjs. The machine checks that judge "this folder holds only what its slot allows"
|
|
7
|
+
* (feature-shape, fe-slot-allows) and the front-end lint rules (through `hfs.allows(file)` of scripts/lib/hfs-view.mjs) read it here so no check keeps a path list.
|
|
8
|
+
*
|
|
9
|
+
* An entry is a name relative to the slot instance root: `<var>` is the instance's bound variable when the slot binds it
|
|
10
|
+
* (`<feature>`, `<capability>`) and one path segment of any text otherwise (`<action>`), `*` and `?` stay inside a segment,
|
|
11
|
+
* `{a,b}` alternates, a trailing `/` names a directory and everything below it.
|
|
12
|
+
*/
|
|
13
|
+
const VAR = /<([A-Za-z][A-Za-z0-9-]*)>/g;
|
|
14
|
+
|
|
15
|
+
function matchersOf(entries, bindings) {
|
|
16
|
+
return (entries ?? []).flatMap(entry => {
|
|
17
|
+
const directory = entry.endsWith('/');
|
|
18
|
+
const body = directory ? entry.slice(0, -1) : entry;
|
|
19
|
+
const filled = body.replace(VAR, (whole, name) => bindings[name] ?? '*');
|
|
20
|
+
return braceVariants(filled).map(variant => ({ entry, directory, expression: globExpression(directory ? `${variant}/**` : variant) }));
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The path of `file` below the instance root of the slot that owns it. */
|
|
25
|
+
export function relativeToRoot(file, root) {
|
|
26
|
+
return root && file.startsWith(`${root}/`) ? file.slice(root.length + 1) : path.posix.basename(file);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Whether the slot that owns `file` names it in `requires` or `allows` (and does not forbid it); null when no enabled slot
|
|
31
|
+
* owns it. Unlike allowsFile it judges a slot that lists only `requires` too, so a folder closed by its slot (an app's
|
|
32
|
+
* src/) is read from the slot and never from a check's own list.
|
|
33
|
+
*/
|
|
34
|
+
export function slotAdmitsFile(resolver, file) {
|
|
35
|
+
const classified = resolver.classifyPath(file);
|
|
36
|
+
if (classified.status === 'no-slot' || classified.status === 'ambiguous' || !classified.slot) return null;
|
|
37
|
+
const slot = resolver.slot(classified.slot);
|
|
38
|
+
const relative = relativeToRoot(classified.path, classified.root);
|
|
39
|
+
const hit = matchersOf([...(slot.requires ?? []), ...(slot.allows ?? [])], classified.bindings).find(matcher => matcher.expression.test(relative));
|
|
40
|
+
const forbidden = matchersOf(slot.forbids, classified.bindings).find(matcher => matcher.expression.test(relative) || matcher.expression.test(`${relative}/`));
|
|
41
|
+
return Boolean(hit) && !forbidden;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* {slot, root, relative, allowed, entry?, forbiddenBy?} for a repository-relative file, or null when no enabled slot owns
|
|
46
|
+
* it or the slot names no `allows`. `allowed` is true when the relative path matches a `requires` or `allows` entry.
|
|
47
|
+
*/
|
|
48
|
+
export function allowsFile(resolver, file) {
|
|
49
|
+
const classified = resolver.classifyPath(file);
|
|
50
|
+
if (classified.status === 'no-slot' || classified.status === 'ambiguous' || !classified.slot) return null;
|
|
51
|
+
const slot = resolver.slot(classified.slot);
|
|
52
|
+
if (!slot?.allows?.length) return null;
|
|
53
|
+
const relative = relativeToRoot(classified.path, classified.root);
|
|
54
|
+
const hit = matchersOf([...(slot.requires ?? []), ...slot.allows], classified.bindings).find(matcher => matcher.expression.test(relative));
|
|
55
|
+
const forbidden = matchersOf(slot.forbids, classified.bindings).find(matcher => matcher.expression.test(relative) || matcher.expression.test(`${relative}/`));
|
|
56
|
+
return { slot: slot.id, root: classified.root, relative, allowed: Boolean(hit) && !forbidden, entry: hit?.entry, forbiddenBy: forbidden?.entry, allows: slot.allows };
|
|
57
|
+
}
|