@starci/hfs 4.0.7 → 4.0.8
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 +13 -0
- package/README.md +10 -9
- package/bin/hfs.mjs +4 -4
- package/lint/run.mjs +1 -1
- package/package.json +2 -2
- package/runtime/engine/runtime-root.mjs +5 -0
- package/runtime/engine/yaml.mjs +3 -3
- package/runtime/knowledge/hfs/canon-pins.yaml +10 -10
- package/runtime/knowledge/hfs/slots.yaml +5 -5
- package/runtime/knowledge/patterns/fe/folder.yaml +2 -2
- package/runtime/knowledge/sonar-gate.yaml +2 -2
- package/runtime/modules/kernel/failure-codes.yaml +12 -2
- package/runtime/scripts/api/fs/lib.mjs +6 -0
- package/runtime/scripts/api/fs/rmdir-link.mjs +1 -1
- package/runtime/scripts/{lib → api/fs}/safe-remove.mjs +31 -82
- package/runtime/scripts/api/git/lib.mjs +1 -1
- package/runtime/scripts/api/sops/decrypt.mjs +1 -1
- package/runtime/scripts/{lib/hfs-allows.mjs → hfs/allows.mjs} +3 -3
- package/runtime/scripts/{checks → hfs}/architecture/backend.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/config.mjs +2 -2
- package/runtime/scripts/{checks → hfs}/architecture/connection-map.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/contract-fixture-guard.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/fe-slot-allows.mjs +3 -3
- package/runtime/scripts/{checks → hfs}/architecture/feature-shape.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/hfs-graph.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/hfs.mjs +3 -3
- package/runtime/scripts/hfs/architecture/next-data-contract.mjs +96 -0
- package/runtime/scripts/{checks → hfs}/architecture/next-data.mjs +18 -105
- package/runtime/scripts/{checks → hfs}/architecture/required-files.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/surface.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/test-world-files.mjs +2 -2
- package/runtime/scripts/{checks → hfs}/architecture/typescript.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture.mjs +2 -2
- package/runtime/scripts/{lib/hfs-check.mjs → hfs/check.mjs} +33 -25
- package/runtime/scripts/hfs/manifest-shape.mjs +170 -0
- package/runtime/scripts/{lib/hfs-path-findings.mjs → hfs/path-findings.mjs} +6 -6
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/contract.mjs +3 -2
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +66 -0
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/integration-specs.mjs +1 -1
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/secrets.mjs +2 -2
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/stacks.mjs +2 -2
- package/runtime/scripts/{lib/hfs-slots.mjs → hfs/slots.mjs} +52 -58
- package/runtime/scripts/{lib/hfs-tree.mjs → hfs/tree.mjs} +1 -1
- package/runtime/scripts/{lib/hfs-view.mjs → hfs/view.mjs} +3 -3
- package/runtime/scripts/lib/graphql-contract.mjs +429 -0
- package/runtime/scripts/lib/is-main.mjs +13 -0
- package/runtime/scripts/lib/language.mjs +2 -2
- package/runtime/scripts/lib/path-key.mjs +2 -0
- package/runtime/scripts/lib/stack-declaration.mjs +2 -2
- package/runtime/scripts/{checks/common.mjs → lib/walk.mjs} +1 -12
- package/scaffold/app.mjs +1 -1
- package/scaffold/service.mjs +2 -2
- package/sync/hygiene.mjs +8 -8
- package/sync/index.mjs +48 -11
- package/templates/app/quality-config/codecov.yml +1 -1
- package/templates/app/quality-config/sonar-project.properties +1 -1
- package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.ts +2 -2
- package/templates/fe/skeleton/apps/__app__/next.config.ts +10 -2
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/error.tsx +2 -2
- package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/component.tsx +0 -2
- package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/index.tsx +1 -7
- package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/component.tsx +0 -2
- package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/index.tsx +0 -1
- package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/component.tsx +0 -4
- package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/index.tsx +1 -1
- package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/component.tsx +0 -4
- package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/index.tsx +1 -1
- package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/component.tsx +0 -4
- package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/index.tsx +1 -1
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/request.ts +8 -3
- package/templates/fe/skeleton/apps/__app__/src/proxy.ts +1 -1
- package/runtime/engine/admission.mjs +0 -284
- package/runtime/engine/digest.mjs +0 -10
- package/runtime/engine/ledger-db.mjs +0 -1245
- package/runtime/engine/machine-db.mjs +0 -1565
- package/runtime/engine/migrations/machine/0001-init.sql +0 -887
- package/runtime/engine/migrations/machine/0002-worktrees-no-workflow-kind.sql +0 -13
- package/runtime/engine/migrations/machine/0003-worktrees-workflow-orca.sql +0 -21
- package/runtime/engine/migrations/runtime/0001-init.sql +0 -1072
- package/runtime/engine/migrations/runtime/0003-usage-unavailable.sql +0 -13
- package/runtime/engine/migrations/runtime/0004-attempt-why.sql +0 -43
- package/runtime/engine/migrations/runtime/0005-ended-workflow-views.sql +0 -93
- package/runtime/scripts/lib/artifact-hold.mjs +0 -89
- package/runtime/scripts/lib/artifact-store.mjs +0 -103
- package/runtime/scripts/lib/redact.mjs +0 -148
- /package/runtime/scripts/{checks → hfs}/architecture/background-unowned.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/client-reaches-server.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/clones.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/config-unread.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/constructor-deps.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/contracts.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/cross-app-duplicate.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/dead-exports.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/default-deny.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/doc-language.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/entrypoint.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/error-codes.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/error-masked.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/framework-pinned.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/frontend.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/hooks-are-hooks.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/i18n-keys.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/index.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/injection-token-exported.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/machine-ast.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/module-per-transport.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/owners.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/package-shape.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/reachability.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/register-once.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/registration.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/route-files-thin.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/schema-owner.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/source-names.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/sql-owner.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/sql-tokens.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/symbols.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/tiers.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/transport-owner.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/unit-spec-providers.mjs +0 -0
- /package/runtime/scripts/{lib → hfs}/repo-identity.mjs +0 -0
- /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/deps.mjs +0 -0
- /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/fe-no-tests.mjs +0 -0
- /package/runtime/scripts/{lib/hfs-rules/frontend.mjs → hfs/rules/frontend-tree.mjs} +0 -0
- /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/lint-suppression.mjs +0 -0
- /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/peer-integrations.mjs +0 -0
- /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/pipeline.mjs +0 -0
- /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/proof-commands.mjs +0 -0
- /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/read.mjs +0 -0
- /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/repo-local-checks.mjs +0 -0
- /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/spec-placement.mjs +0 -0
- /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/test-topology.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/typescript-programs.mjs +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// decrypt.mjs — one `sops` decryption: the custody reads (scripts/
|
|
1
|
+
// decrypt.mjs — one `sops` decryption: the custody reads (scripts/api/sops/exec-env.mjs execEnv) and the test
|
|
2
2
|
// secrets (scripts/lib/test-secrets.mjs testSecret). The plaintext stays in this process's memory (stdout over a pipe).
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { braceVariants, globExpression } from '
|
|
2
|
+
import { braceVariants, globExpression } from '../lib/glob.mjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
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/
|
|
7
|
-
* (feature-shape, fe-slot-allows) and the front-end lint rules (through `hfs.allows(file)` of scripts/
|
|
6
|
+
* resolver of scripts/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/hfs/view.mjs) read it here so no check keeps a path list.
|
|
8
8
|
*
|
|
9
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
10
|
* (`<feature>`, `<capability>`) and one path segment of any text otherwise (`<action>`), `*` and `?` stay inside a segment,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { isInside } from './config.mjs';
|
|
3
|
-
import { slotAdmitsFile } from '
|
|
3
|
+
import { slotAdmitsFile } from '../allows.mjs';
|
|
4
4
|
import { reachableViolation, relativePath, sourceLocation } from './typescript.mjs';
|
|
5
5
|
|
|
6
6
|
const FORBIDDEN_APP_ROLE = /(?:^|\.)(?:service|provider|providers|resolver|controller|handler|repository|entity|use-case|command|query|listener|consumer|processor)\.[cm]?[jt]sx?$/i;
|
|
@@ -2,8 +2,8 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { slash } from '../../lib/path-key.mjs';
|
|
4
4
|
import { readJsonFile as readJson } from '../../lib/json.mjs';
|
|
5
|
-
import { isInside } from '
|
|
6
|
-
import { openHfs } from '
|
|
5
|
+
import { isInside } from '../../lib/walk.mjs';
|
|
6
|
+
import { openHfs } from '../slots.mjs';
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
/** A file's identity: its resolved real path, or the resolved path when it does not exist. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { machineKit, pascal, upperSnake } from './machine-ast.mjs';
|
|
4
|
-
import { locateDeclaration } from '
|
|
4
|
+
import { locateDeclaration } from '../slots.mjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* R84 `connection-map` (BE_CONNECTION_DUPLICATE), folding RED01 and RED02. One physical database is one connection, one
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { treeOf } from './required-files.mjs';
|
|
4
|
-
import { allowsFile } from '
|
|
4
|
+
import { allowsFile } from '../allows.mjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* R47 `contract-fixture-guard` (BE_CONTRACT_UNGUARDED). A fake at the network edge serves payload fixtures; a fixture drifts
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { treeOf } from './required-files.mjs';
|
|
2
|
-
import { allowsFile } from '
|
|
3
|
-
import { isFeTestPath } from '
|
|
2
|
+
import { allowsFile } from '../allows.mjs';
|
|
3
|
+
import { isFeTestPath } from '../rules/fe-no-tests.mjs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* R94 `fe-slot-allows` (FE_SLOT_FILE_ROLE). A front-end slot that owns a whole directory (`fe.route`, `fe.feature`,
|
|
@@ -11,7 +11,7 @@ import { isFeTestPath } from '../../lib/hfs-rules/fe-no-tests.mjs';
|
|
|
11
11
|
* Files no slot owns are HFS_PATH_NO_SLOT's (`hfs check`), so together every front-end file is placed by exactly one slot and
|
|
12
12
|
* named by it. A test path (a `.spec.tsx` or `.test.tsx` beside a component, a test directory or test tooling) is FE_NO_TESTS's
|
|
13
13
|
* (R97, contract-change fe-no-tests): that rule is the one finding of such a file, so this check leaves it alone, as
|
|
14
|
-
* HFS_PATH_NO_SLOT does (scripts/
|
|
14
|
+
* HFS_PATH_NO_SLOT does (scripts/hfs/path-findings.mjs).
|
|
15
15
|
*/
|
|
16
16
|
export const FE_SLOT_ALLOWS_RULE_IDS = ['FE_SLOT_FILE_ROLE'];
|
|
17
17
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { treeOf } from './required-files.mjs';
|
|
3
|
-
import { allowsFile } from '
|
|
3
|
+
import { allowsFile } from '../allows.mjs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* R29 `feature-shape` (BE_FEATURE_SHAPE). A feature root holds `index.ts`, `<feature>.module.ts`, `application/`,
|
|
@@ -3,7 +3,7 @@ import { relativePath } from './typescript.mjs';
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The file and owner graph the HFS architecture checks share. Every production source file the TypeScript context
|
|
6
|
-
* loaded becomes a node classified by the slot manifest (config.hfs is the resolver of scripts/
|
|
6
|
+
* loaded becomes a node classified by the slot manifest (config.hfs is the resolver of scripts/hfs/slots.mjs);
|
|
7
7
|
* every import, re-export and type-only import between two of them becomes an edge. Nothing here judges: the checks
|
|
8
8
|
* in tiers.mjs, reachability.mjs, dead-exports.mjs, required-files.mjs and clones.mjs read it.
|
|
9
9
|
*
|
|
@@ -2,10 +2,10 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { isIP } from 'node:net';
|
|
4
4
|
import { gitOutput } from '../../api/git/lib.mjs';
|
|
5
|
-
import { repositoryName } from '
|
|
5
|
+
import { repositoryName } from '../repo-identity.mjs';
|
|
6
6
|
import { braceVariants } from '../../lib/glob.mjs';
|
|
7
|
-
import { createSlotResolver, loadSlotManifest, openHfs } from '
|
|
8
|
-
import { isFeTestPath } from '
|
|
7
|
+
import { createSlotResolver, loadSlotManifest, openHfs } from '../slots.mjs';
|
|
8
|
+
import { isFeTestPath } from '../rules/fe-no-tests.mjs';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* HFS repository-tree check (knowledge/hfs/README.md): every StarCi repository is an
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { canonical, exactKeys, isInside, slash } from './config.mjs';
|
|
5
|
+
|
|
6
|
+
const CONTRACT_SCHEMA = 'starci/next-data-lifecycle@1';
|
|
7
|
+
const SOURCE = /\.[cm]?[jt]sx?$/i;
|
|
8
|
+
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
9
|
+
const BINDING = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/;
|
|
10
|
+
const TOP_KEYS = new Set(['schema', 'swr', 'hooks']);
|
|
11
|
+
const SWR_KEYS = new Set(['package', 'major']);
|
|
12
|
+
const HOOK_KEYS = new Set(['id', 'path', 'export', 'kind', 'resultBinding', 'identities']);
|
|
13
|
+
const IDENTITY_KEYS = new Set(['id', 'binding', 'gatesRequest', 'resource']);
|
|
14
|
+
function relativeSource(repository, value, label) {
|
|
15
|
+
repository = path.resolve(repository);
|
|
16
|
+
if (typeof value !== 'string' || !value.trim() || path.isAbsolute(value)) throw Error(`${label} must be a non-empty repository-relative source path.`);
|
|
17
|
+
const relative = slash(value.trim()).replace(/^\.\//, '');
|
|
18
|
+
if (relative.split('/').includes('..') || !SOURCE.test(relative) || /\.d\.[cm]?[jt]s$/i.test(relative)) {
|
|
19
|
+
throw Error(`${label} must be a production TypeScript or JavaScript source path.`);
|
|
20
|
+
}
|
|
21
|
+
const absolute = path.resolve(repository, ...relative.split('/'));
|
|
22
|
+
if (!isInside(repository, absolute)) throw Error(`${label} must stay inside the repository.`);
|
|
23
|
+
for (let current = absolute; current !== repository; current = path.dirname(current)) {
|
|
24
|
+
const stat = fs.lstatSync(current);
|
|
25
|
+
if (stat.isSymbolicLink()) throw Error(`${label} must not cross a symbolic-link or junction ancestor.`);
|
|
26
|
+
}
|
|
27
|
+
const stat = fs.lstatSync(absolute);
|
|
28
|
+
if (!stat.isFile() || !isInside(canonical(repository), canonical(absolute))) throw Error(`${label} must be a regular source file inside the repository.`);
|
|
29
|
+
return relative;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function parseContract(config) {
|
|
33
|
+
// The contract is declared in the app's one package.json (the app root; config.root is the fe side folder).
|
|
34
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(config.packageRoot ?? config.root, 'package.json'), 'utf8'));
|
|
35
|
+
const next = manifest?.starci?.codePatterns?.next;
|
|
36
|
+
const value = next?.dataLifecycle;
|
|
37
|
+
if (value === undefined) return null;
|
|
38
|
+
if (next.schema !== 'starci/next-code-pattern-contract@1') throw Error('package.json#starci.codePatterns.next must use starci/next-code-pattern-contract@1.');
|
|
39
|
+
exactKeys(value, TOP_KEYS, 'package.json#starci.codePatterns.next.dataLifecycle');
|
|
40
|
+
if (value.schema !== CONTRACT_SCHEMA) throw Error(`dataLifecycle.schema must be ${CONTRACT_SCHEMA}.`);
|
|
41
|
+
exactKeys(value.swr, SWR_KEYS, 'dataLifecycle.swr');
|
|
42
|
+
if (value.swr.package !== 'swr' || !Number.isInteger(value.swr.major) || value.swr.major < 1) {
|
|
43
|
+
throw Error('dataLifecycle.swr must bind package swr and one positive integer major version.');
|
|
44
|
+
}
|
|
45
|
+
if (!Array.isArray(value.hooks) || value.hooks.length === 0) throw Error('dataLifecycle.hooks must contain at least one declared lifecycle call.');
|
|
46
|
+
const ids = new Set();
|
|
47
|
+
const hooks = value.hooks.map((hook, index) => {
|
|
48
|
+
const label = `dataLifecycle.hooks[${index}]`;
|
|
49
|
+
exactKeys(hook, HOOK_KEYS, label);
|
|
50
|
+
if (typeof hook.id !== 'string' || !hook.id.trim() || ids.has(hook.id.trim())) throw Error(`${label}.id must be unique and non-empty.`);
|
|
51
|
+
ids.add(hook.id.trim());
|
|
52
|
+
const source = relativeSource(config.root, hook.path, `${label}.path`);
|
|
53
|
+
if (typeof hook.export !== 'string' || !/^use[A-Z0-9_$][\w$]*$/.test(hook.export)) throw Error(`${label}.export must name one exported use* hook.`);
|
|
54
|
+
if (!['query', 'mutation'].includes(hook.kind)) throw Error(`${label}.kind must be query or mutation.`);
|
|
55
|
+
if (hook.resultBinding !== undefined && (typeof hook.resultBinding !== 'string' || !IDENTIFIER.test(hook.resultBinding))) {
|
|
56
|
+
throw Error(`${label}.resultBinding must be one local identifier.`);
|
|
57
|
+
}
|
|
58
|
+
if (!Array.isArray(hook.identities)) throw Error(`${label}.identities must be an array.`);
|
|
59
|
+
const identityIds = new Set(), bindings = new Set();
|
|
60
|
+
const identities = hook.identities.map((identity, identityIndex) => {
|
|
61
|
+
const identityLabel = `${label}.identities[${identityIndex}]`;
|
|
62
|
+
exactKeys(identity, IDENTITY_KEYS, identityLabel);
|
|
63
|
+
if (typeof identity.id !== 'string' || !identity.id.trim() || identityIds.has(identity.id.trim())) throw Error(`${identityLabel}.id must be unique and non-empty.`);
|
|
64
|
+
if (typeof identity.binding !== 'string' || !BINDING.test(identity.binding) || bindings.has(identity.binding)) throw Error(`${identityLabel}.binding must be a unique identifier or property path.`);
|
|
65
|
+
if (typeof identity.gatesRequest !== 'boolean' || typeof identity.resource !== 'boolean') throw Error(`${identityLabel} must explicitly declare gatesRequest and resource booleans.`);
|
|
66
|
+
identityIds.add(identity.id.trim());
|
|
67
|
+
bindings.add(identity.binding);
|
|
68
|
+
return { id: identity.id.trim(), binding: identity.binding, gatesRequest: identity.gatesRequest, resource: identity.resource };
|
|
69
|
+
});
|
|
70
|
+
if (hook.kind === 'mutation' && !identities.some(identity => identity.resource)) throw Error(`${label} mutation must declare at least one resource identity.`);
|
|
71
|
+
return { id: hook.id.trim(), path: source, export: hook.export, kind: hook.kind,
|
|
72
|
+
...(hook.resultBinding === undefined ? {} : { resultBinding: hook.resultBinding }), identities };
|
|
73
|
+
});
|
|
74
|
+
return { schema: value.schema, swr: { package: 'swr', major: value.swr.major }, hooks };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function installedSWR(repository) {
|
|
78
|
+
const require = createRequire(path.join(repository, 'package.json'));
|
|
79
|
+
let manifest;
|
|
80
|
+
try { manifest = require.resolve('swr/package.json'); } catch {
|
|
81
|
+
let entry;
|
|
82
|
+
try { entry = require.resolve('swr'); } catch { throw Error('Installed swr package is unavailable.'); }
|
|
83
|
+
let current = path.dirname(entry);
|
|
84
|
+
while (isInside(repository, current) && current !== repository) {
|
|
85
|
+
const candidate = path.join(current, 'package.json');
|
|
86
|
+
if (fs.existsSync(candidate)) { manifest = candidate; break; }
|
|
87
|
+
current = path.dirname(current);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (!manifest) throw Error('Installed swr package.json is unavailable.');
|
|
91
|
+
const actualManifest = canonical(manifest);
|
|
92
|
+
const parsed = JSON.parse(fs.readFileSync(actualManifest, 'utf8'));
|
|
93
|
+
const major = Number.parseInt(String(parsed.version ?? '').split('.')[0], 10);
|
|
94
|
+
if (parsed.name !== 'swr' || !Number.isInteger(major)) throw Error('Installed swr package identity or version is invalid.');
|
|
95
|
+
return { name: parsed.name, version: parsed.version, major, root: canonical(path.dirname(actualManifest)) };
|
|
96
|
+
}
|
|
@@ -1,107 +1,14 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
1
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { canonical, isInside } from './config.mjs';
|
|
3
|
+
import { parseContract, installedSWR } from './next-data-contract.mjs';
|
|
5
4
|
import { relativePath, sourceLocation, unwrapExpression } from './typescript.mjs';
|
|
6
5
|
|
|
7
6
|
export const SWR_KEY_RULE_ID = 'FE_SWR_KEY_IDENTITY';
|
|
8
7
|
export const SWR_MUTATION_RULE_ID = 'FE_SWR_MUTATION_RESOURCE_IDENTITY';
|
|
9
8
|
export const SWR_DATA_RULE_IDS = [SWR_KEY_RULE_ID, SWR_MUTATION_RULE_ID];
|
|
10
9
|
|
|
11
|
-
const CONTRACT_SCHEMA = 'starci/next-data-lifecycle@1';
|
|
12
|
-
const SOURCE = /\.[cm]?[jt]sx?$/i;
|
|
13
|
-
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
14
|
-
const BINDING = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/;
|
|
15
|
-
const TOP_KEYS = new Set(['schema', 'swr', 'hooks']);
|
|
16
|
-
const SWR_KEYS = new Set(['package', 'major']);
|
|
17
|
-
const HOOK_KEYS = new Set(['id', 'path', 'export', 'kind', 'resultBinding', 'identities']);
|
|
18
|
-
const IDENTITY_KEYS = new Set(['id', 'binding', 'gatesRequest', 'resource']);
|
|
19
10
|
const SWR_SPECIFIERS = new Set(['swr', 'swr/immutable', 'swr/mutation']);
|
|
20
11
|
|
|
21
|
-
function relativeSource(repository, value, label) {
|
|
22
|
-
repository = path.resolve(repository);
|
|
23
|
-
if (typeof value !== 'string' || !value.trim() || path.isAbsolute(value)) throw Error(`${label} must be a non-empty repository-relative source path.`);
|
|
24
|
-
const relative = slash(value.trim()).replace(/^\.\//, '');
|
|
25
|
-
if (relative.split('/').includes('..') || !SOURCE.test(relative) || /\.d\.[cm]?[jt]s$/i.test(relative)) {
|
|
26
|
-
throw Error(`${label} must be a production TypeScript or JavaScript source path.`);
|
|
27
|
-
}
|
|
28
|
-
const absolute = path.resolve(repository, ...relative.split('/'));
|
|
29
|
-
if (!isInside(repository, absolute)) throw Error(`${label} must stay inside the repository.`);
|
|
30
|
-
for (let current = absolute; current !== repository; current = path.dirname(current)) {
|
|
31
|
-
const stat = fs.lstatSync(current);
|
|
32
|
-
if (stat.isSymbolicLink()) throw Error(`${label} must not cross a symbolic-link or junction ancestor.`);
|
|
33
|
-
}
|
|
34
|
-
const stat = fs.lstatSync(absolute);
|
|
35
|
-
if (!stat.isFile() || !isInside(canonical(repository), canonical(absolute))) throw Error(`${label} must be a regular source file inside the repository.`);
|
|
36
|
-
return relative;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function parseContract(config) {
|
|
40
|
-
// The contract is declared in the app's one package.json (the app root; config.root is the fe side folder).
|
|
41
|
-
const manifest = JSON.parse(fs.readFileSync(path.join(config.packageRoot ?? config.root, 'package.json'), 'utf8'));
|
|
42
|
-
const next = manifest?.starci?.codePatterns?.next;
|
|
43
|
-
const value = next?.dataLifecycle;
|
|
44
|
-
if (value === undefined) return null;
|
|
45
|
-
if (next.schema !== 'starci/next-code-pattern-contract@1') throw Error('package.json#starci.codePatterns.next must use starci/next-code-pattern-contract@1.');
|
|
46
|
-
exactKeys(value, TOP_KEYS, 'package.json#starci.codePatterns.next.dataLifecycle');
|
|
47
|
-
if (value.schema !== CONTRACT_SCHEMA) throw Error(`dataLifecycle.schema must be ${CONTRACT_SCHEMA}.`);
|
|
48
|
-
exactKeys(value.swr, SWR_KEYS, 'dataLifecycle.swr');
|
|
49
|
-
if (value.swr.package !== 'swr' || !Number.isInteger(value.swr.major) || value.swr.major < 1) {
|
|
50
|
-
throw Error('dataLifecycle.swr must bind package swr and one positive integer major version.');
|
|
51
|
-
}
|
|
52
|
-
if (!Array.isArray(value.hooks) || value.hooks.length === 0) throw Error('dataLifecycle.hooks must contain at least one declared lifecycle call.');
|
|
53
|
-
const ids = new Set();
|
|
54
|
-
const hooks = value.hooks.map((hook, index) => {
|
|
55
|
-
const label = `dataLifecycle.hooks[${index}]`;
|
|
56
|
-
exactKeys(hook, HOOK_KEYS, label);
|
|
57
|
-
if (typeof hook.id !== 'string' || !hook.id.trim() || ids.has(hook.id.trim())) throw Error(`${label}.id must be unique and non-empty.`);
|
|
58
|
-
ids.add(hook.id.trim());
|
|
59
|
-
const source = relativeSource(config.root, hook.path, `${label}.path`);
|
|
60
|
-
if (typeof hook.export !== 'string' || !/^use[A-Z0-9_$][\w$]*$/.test(hook.export)) throw Error(`${label}.export must name one exported use* hook.`);
|
|
61
|
-
if (!['query', 'mutation'].includes(hook.kind)) throw Error(`${label}.kind must be query or mutation.`);
|
|
62
|
-
if (hook.resultBinding !== undefined && (typeof hook.resultBinding !== 'string' || !IDENTIFIER.test(hook.resultBinding))) {
|
|
63
|
-
throw Error(`${label}.resultBinding must be one local identifier.`);
|
|
64
|
-
}
|
|
65
|
-
if (!Array.isArray(hook.identities)) throw Error(`${label}.identities must be an array.`);
|
|
66
|
-
const identityIds = new Set(), bindings = new Set();
|
|
67
|
-
const identities = hook.identities.map((identity, identityIndex) => {
|
|
68
|
-
const identityLabel = `${label}.identities[${identityIndex}]`;
|
|
69
|
-
exactKeys(identity, IDENTITY_KEYS, identityLabel);
|
|
70
|
-
if (typeof identity.id !== 'string' || !identity.id.trim() || identityIds.has(identity.id.trim())) throw Error(`${identityLabel}.id must be unique and non-empty.`);
|
|
71
|
-
if (typeof identity.binding !== 'string' || !BINDING.test(identity.binding) || bindings.has(identity.binding)) throw Error(`${identityLabel}.binding must be a unique identifier or property path.`);
|
|
72
|
-
if (typeof identity.gatesRequest !== 'boolean' || typeof identity.resource !== 'boolean') throw Error(`${identityLabel} must explicitly declare gatesRequest and resource booleans.`);
|
|
73
|
-
identityIds.add(identity.id.trim());
|
|
74
|
-
bindings.add(identity.binding);
|
|
75
|
-
return { id: identity.id.trim(), binding: identity.binding, gatesRequest: identity.gatesRequest, resource: identity.resource };
|
|
76
|
-
});
|
|
77
|
-
if (hook.kind === 'mutation' && !identities.some(identity => identity.resource)) throw Error(`${label} mutation must declare at least one resource identity.`);
|
|
78
|
-
return { id: hook.id.trim(), path: source, export: hook.export, kind: hook.kind,
|
|
79
|
-
...(hook.resultBinding === undefined ? {} : { resultBinding: hook.resultBinding }), identities };
|
|
80
|
-
});
|
|
81
|
-
return { schema: value.schema, swr: { package: 'swr', major: value.swr.major }, hooks };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function installedSWR(repository) {
|
|
85
|
-
const require = createRequire(path.join(repository, 'package.json'));
|
|
86
|
-
let manifest;
|
|
87
|
-
try { manifest = require.resolve('swr/package.json'); } catch {
|
|
88
|
-
let entry;
|
|
89
|
-
try { entry = require.resolve('swr'); } catch { throw Error('Installed swr package is unavailable.'); }
|
|
90
|
-
let current = path.dirname(entry);
|
|
91
|
-
while (isInside(repository, current) && current !== repository) {
|
|
92
|
-
const candidate = path.join(current, 'package.json');
|
|
93
|
-
if (fs.existsSync(candidate)) { manifest = candidate; break; }
|
|
94
|
-
current = path.dirname(current);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
if (!manifest) throw Error('Installed swr package.json is unavailable.');
|
|
98
|
-
const actualManifest = canonical(manifest);
|
|
99
|
-
const parsed = JSON.parse(fs.readFileSync(actualManifest, 'utf8'));
|
|
100
|
-
const major = Number.parseInt(String(parsed.version ?? '').split('.')[0], 10);
|
|
101
|
-
if (parsed.name !== 'swr' || !Number.isInteger(major)) throw Error('Installed swr package identity or version is invalid.');
|
|
102
|
-
return { name: parsed.name, version: parsed.version, major, root: canonical(path.dirname(actualManifest)) };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
12
|
function normalizedSymbol(ts, checker, value) {
|
|
106
13
|
let symbol = value ?? null;
|
|
107
14
|
const seen = new Set();
|
|
@@ -543,22 +450,29 @@ function nullish(ts, checker, expression) {
|
|
|
543
450
|
return !symbol || (symbol.getDeclarations?.() ?? []).every(declaration => declaration.getSourceFile().isDeclarationFile);
|
|
544
451
|
}
|
|
545
452
|
|
|
546
|
-
|
|
453
|
+
/** True when taking `branch` of `condition` proves the declared identity is available. */
|
|
454
|
+
function branchMeansAvailable(ts, checker, condition, identity, branch) {
|
|
547
455
|
condition = unwrapExpression(ts, condition);
|
|
548
456
|
if (expressionReferences(ts, checker, condition, identity) && (ts.isIdentifier(condition)
|
|
549
|
-
|| ts.isPropertyAccessExpression(condition) || ts.isElementAccessExpression(condition))) return
|
|
457
|
+
|| ts.isPropertyAccessExpression(condition) || ts.isElementAccessExpression(condition))) return branch;
|
|
550
458
|
if (ts.isPrefixUnaryExpression(condition) && condition.operator === ts.SyntaxKind.ExclamationToken) {
|
|
551
|
-
|
|
552
|
-
return nested === null ? null : !nested;
|
|
459
|
+
return branchMeansAvailable(ts, checker, condition.operand, identity, !branch);
|
|
553
460
|
}
|
|
554
461
|
if (ts.isBinaryExpression(condition)) {
|
|
462
|
+
const operator = condition.operatorToken.kind;
|
|
463
|
+
if (operator === ts.SyntaxKind.AmpersandAmpersandToken || operator === ts.SyntaxKind.BarBarToken) {
|
|
464
|
+
// `a && b` is true only when both sides are true; `a || b` is false only when both sides are false.
|
|
465
|
+
const decisive = operator === ts.SyntaxKind.AmpersandAmpersandToken ? branch : !branch;
|
|
466
|
+
return decisive && [condition.left, condition.right]
|
|
467
|
+
.some(side => branchMeansAvailable(ts, checker, side, identity, branch));
|
|
468
|
+
}
|
|
555
469
|
const leftIdentity = expressionReferences(ts, checker, condition.left, identity) && nullish(ts, checker, condition.right);
|
|
556
470
|
const rightIdentity = expressionReferences(ts, checker, condition.right, identity) && nullish(ts, checker, condition.left);
|
|
557
|
-
if (!leftIdentity && !rightIdentity) return
|
|
558
|
-
if ([ts.SyntaxKind.ExclamationEqualsToken, ts.SyntaxKind.ExclamationEqualsEqualsToken].includes(
|
|
559
|
-
if ([ts.SyntaxKind.EqualsEqualsToken, ts.SyntaxKind.EqualsEqualsEqualsToken].includes(
|
|
471
|
+
if (!leftIdentity && !rightIdentity) return false;
|
|
472
|
+
if ([ts.SyntaxKind.ExclamationEqualsToken, ts.SyntaxKind.ExclamationEqualsEqualsToken].includes(operator)) return branch;
|
|
473
|
+
if ([ts.SyntaxKind.EqualsEqualsToken, ts.SyntaxKind.EqualsEqualsEqualsToken].includes(operator)) return !branch;
|
|
560
474
|
}
|
|
561
|
-
return
|
|
475
|
+
return false;
|
|
562
476
|
}
|
|
563
477
|
|
|
564
478
|
function violation(config, call, ruleId, message, extra = {}) {
|
|
@@ -687,8 +601,7 @@ function inspectKey(config, context, entry, call, identities, violations, reason
|
|
|
687
601
|
}
|
|
688
602
|
if (identity.gatesRequest) {
|
|
689
603
|
const gated = active.every(leaf => leaf.decisions.some(decision => {
|
|
690
|
-
|
|
691
|
-
return available !== null && decision.branch === available;
|
|
604
|
+
return branchMeansAvailable(context.ts, call.checker, decision.condition, identity, decision.branch);
|
|
692
605
|
}));
|
|
693
606
|
if (!gated) violations.push(violation(config, call, SWR_KEY_RULE_ID,
|
|
694
607
|
`${entry.id} must produce an explicit null key when ${identity.id} (${identity.binding}) is unavailable.`,
|
|
@@ -5,7 +5,7 @@ import { gitOutput } from '../../api/git/lib.mjs';
|
|
|
5
5
|
/**
|
|
6
6
|
* HFS check 5: the files and directories the slot manifest requires (knowledge/hfs/slots.yaml `requires`,
|
|
7
7
|
* `requiredInstances`, `minInstances`, required app kinds) must exist in the repository tree. Everything is read through the
|
|
8
|
-
* resolver of scripts/
|
|
8
|
+
* resolver of scripts/hfs/slots.mjs; nothing here names a path.
|
|
9
9
|
*
|
|
10
10
|
* BE_REQUIRED_MODULE_MISSING a backend app, feature, domain, integrations or platform instance lacks a required file or
|
|
11
11
|
* directory, or a required platform instance (config, logging, errors, primitives) is absent
|
|
@@ -60,7 +60,7 @@ export const LINT_ENFORCERS = Object.freeze([
|
|
|
60
60
|
enforcer('component-purity', FE, ['FE_COMPONENT_WORLD_OWNERSHIP', 'FE_WORLD_OWNER_RENDER_BOUNDARY', 'FE_CONNECTED_BLOCK_RENDER_PAIR', 'FE_PURE_REACHES_DATA', 'FE_PURE_WORLD_HOOK', 'FE_PURE_WORLD_IMPORT'], 'A pure component reaches no data or world state; its connected entry owns them.'),
|
|
61
61
|
enforcer('grammar-entry', FE, ['ARCH_GRAMMAR_CONTRACT_INVALID', 'ARCH_GRAMMAR_EXPORT_BYPASS'], 'Vendor grammar is reached only through its owner entry.'),
|
|
62
62
|
enforcer('i18n-keys', FE, ['FE_I18N_KEYS'], 'Translation keys read by source and keys held by catalogs agree both ways.'),
|
|
63
|
-
// The per-path judgements of the slot manifest (scripts/
|
|
63
|
+
// The per-path judgements of the slot manifest (scripts/hfs/path-findings.mjs), on a tracked TypeScript file.
|
|
64
64
|
enforcer('slot-undeclared', BOTH, ['HFS_SLOT_UNDECLARED', 'HFS_SLOT_AMBIGUOUS', 'HFS_SLOT_NOT_ENABLED'], 'A TypeScript file is owned by exactly one slot of the repository.', { origin: 'repo' }),
|
|
65
65
|
enforcer('source-suffix', BE, ['BE_SOURCE_FORM'], 'A source file name is index.ts, main.ts, a migration or <kebab-name>.<suffix>.ts with a suffix of the closed list.', { origin: 'repo' }),
|
|
66
66
|
enforcer('spec-placement', BE, ['BE_SPEC_PLACEMENT'], 'A spec file lives in one of the four test layers and nowhere else.', { origin: 'repo' }),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { treeOf } from './required-files.mjs';
|
|
2
|
-
import { allowsFile } from '
|
|
3
|
-
import { locateDeclaration } from '
|
|
2
|
+
import { allowsFile } from '../allows.mjs';
|
|
3
|
+
import { locateDeclaration } from '../slots.mjs';
|
|
4
4
|
import { DEFAULT_ENVIRONMENT, STACKS_DIRECTORY, STATEFUL_KINDS, namesOfService, readStack } from '../../lib/stack-services.mjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -5,7 +5,7 @@ import { canonical, isInside, slash } from './config.mjs';
|
|
|
5
5
|
import { sameOrUnder } from '../../lib/path-key.mjs';
|
|
6
6
|
import { createTypeScriptProgram, readTypeScriptProject, resolveTypeScriptModule, sharedInProgramRun, typeScriptProjectReferencePath } from '../typescript-programs.mjs';
|
|
7
7
|
import { readJsonFile } from '../../lib/json.mjs';
|
|
8
|
-
import { locateDeclaration } from '
|
|
8
|
+
import { locateDeclaration } from '../slots.mjs';
|
|
9
9
|
|
|
10
10
|
const CODE_EXTENSIONS = /\.(?:[cm]?[jt]sx?)$/i;
|
|
11
11
|
const TEST_FILE = /(?:^|[.-])(?:spec|test)\.[cm]?[jt]sx?$/i;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// architecture.mjs — the architecture check (docs/architecture-check.md).
|
|
2
2
|
//
|
|
3
|
-
// node scripts/
|
|
3
|
+
// node scripts/hfs/architecture.mjs <repo-root> [--base <commit>]
|
|
4
4
|
//
|
|
5
5
|
// Prints one starci/architecture-check@1 record. Exit 0: ok. Exit 1: violations or errors (a check that
|
|
6
6
|
// cannot run is an error, never a pass). Exit 2: bad arguments.
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import {fileURLToPath} from 'node:url';
|
|
9
9
|
import {checkArchitecture} from './architecture/index.mjs';
|
|
10
|
-
import { isMain } from '
|
|
10
|
+
import { isMain } from '../lib/is-main.mjs';
|
|
11
11
|
|
|
12
12
|
export {checkArchitecture};
|
|
13
13
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// hfs-check.mjs - the HFS app check, behind `hfs check | explain` (packages/hfs) and reusable by any
|
|
2
2
|
// runtime check. It reads three things and nothing else: the app's hfs.json, the slot manifest
|
|
3
|
-
// (knowledge/hfs/slots.yaml through scripts/
|
|
3
|
+
// (knowledge/hfs/slots.yaml through scripts/hfs/slots.mjs) and the pins (knowledge/hfs/canon-pins.yaml). It never
|
|
4
4
|
// writes to the repository it inspects.
|
|
5
5
|
//
|
|
6
6
|
// checkRepo() answers, for the tracked paths of one app (git ls-files), per scope (the app root and each side folder):
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
// HFS_PROOF_COMMAND_FILE_MISSING (R105, hfs-rules/proof-commands.mjs) a .starciwork proof command that runs a file the repository does not hold
|
|
29
29
|
// HFS_PEER_INTEGRATION_MISSING (R111, hfs-rules/peer-integrations.mjs) the app root package.json lacks the runtime peer a driver integration needs
|
|
30
30
|
// BE_INTEGRATION_SPEC_MISSING (R112, hfs-rules/integration-specs.mjs) an integration with no integration spec that registers its module, maps its refusals and drives an outage
|
|
31
|
+
// FE_GRAPHQL_CONTRACT (R113, hfs-rules/fe-contract-documents.mjs) a front-end GraphQL document the back end's contract snapshot does not serve
|
|
31
32
|
// FE_WIRE_GENERATED, FE_I18N_PLACEMENT, FE_I18N_CATALOG (R52, R59, R60, hfs-rules/frontend.mjs) the front-end tree of each app
|
|
32
33
|
// HFS_GITIGNORE_BLOCK_DRIFT, HFS_SONAR_CONFIG (R04, R11) produced by packages/hfs/sync/managed.mjs, which renders the templates
|
|
33
34
|
// HFS_FORMAT (R19) produced by packages/hfs/sync/format.mjs, which runs the repository's own prettier
|
|
@@ -38,37 +39,41 @@
|
|
|
38
39
|
// HFS_EMPTY_DIR a directory with no file below it (git never tracks one), outside .git, node_modules and ignored slots
|
|
39
40
|
// HFS_GHOST_TREE an empty directory beside a sibling within two edits of its name (business / bussiness)
|
|
40
41
|
// HFS_UNTRACKED_ROOT_ENTRY an entry git neither tracks nor ignores, outside an `ignored` slot (R03)
|
|
41
|
-
//
|
|
42
|
+
// A runtime repository (hfs.json kind runtime, judged with knowledge/hfs/runtime-slots.yaml) gets the per-path slot findings,
|
|
43
|
+
// the required files and minimum instances and the tree findings only; scripts/hfs/runtime-check.mjs adds the runtime rules.
|
|
44
|
+
// checkRepository() is the whole `hfs check`: checkRepo() plus the architecture machine (scripts/hfs/architecture.mjs, one
|
|
42
45
|
// implementation; the published bundle carries a byte copy), its violations and errors reported as findings under their own
|
|
43
46
|
// codes; `fast` limits both to the owners changed since the merge-base. Every finding carries its code and the Vietnamese why text of modules/kernel/failure-codes.yaml. Only `error`
|
|
44
47
|
// findings fail the check.
|
|
45
48
|
import fs from 'node:fs';
|
|
46
49
|
import path from 'node:path';
|
|
47
50
|
import { skillRoot } from '../../engine/runtime-root.mjs';
|
|
48
|
-
import { ARCHITECTURE_RULE_IDS, checkArchitecture } from '
|
|
51
|
+
import { ARCHITECTURE_RULE_IDS, checkArchitecture } from './architecture/index.mjs';
|
|
49
52
|
import { parseYaml } from '../../engine/yaml.mjs';
|
|
50
|
-
import { APP_SCOPE, HFS_DECLARATION_FILE, appRelativeMessages, HfsSlotsError, SIDES, createSlotResolver, loadSlotManifest, readRepoDeclaration, resolveRepoDeclaration } from './
|
|
51
|
-
import { allowsFile } from './
|
|
53
|
+
import { APP_SCOPE, HFS_DECLARATION_FILE, appRelativeMessages, HfsSlotsError, SIDES, createSlotResolver, loadSlotManifest, readRepoDeclaration, resolveRepoDeclaration } from './slots.mjs';
|
|
54
|
+
import { allowsFile } from './allows.mjs';
|
|
55
|
+
import { RUNTIME_KIND } from './manifest-shape.mjs';
|
|
52
56
|
import { gitOutput } from '../api/git/lib.mjs';
|
|
53
|
-
import { posixPath } from '
|
|
54
|
-
import { readTree, treeFacts, untrackedEntries } from './
|
|
55
|
-
import { contractFindings } from './
|
|
56
|
-
import { depFindings } from './
|
|
57
|
-
import { appFrontendFindings, frontendFindings } from './
|
|
58
|
-
import {
|
|
59
|
-
import {
|
|
60
|
-
import {
|
|
61
|
-
import {
|
|
62
|
-
import {
|
|
63
|
-
import {
|
|
64
|
-
import {
|
|
65
|
-
import {
|
|
66
|
-
import {
|
|
67
|
-
import {
|
|
68
|
-
import {
|
|
69
|
-
import {
|
|
70
|
-
import {
|
|
71
|
-
import {
|
|
57
|
+
import { posixPath } from '../lib/path-key.mjs';
|
|
58
|
+
import { readTree, treeFacts, untrackedEntries } from './tree.mjs';
|
|
59
|
+
import { contractFindings } from './rules/contract.mjs';
|
|
60
|
+
import { depFindings } from './rules/deps.mjs';
|
|
61
|
+
import { appFrontendFindings, frontendFindings } from './rules/frontend-tree.mjs';
|
|
62
|
+
import { feContractFindings } from './rules/fe-contract-documents.mjs';
|
|
63
|
+
import { integrationSpecFindings } from './rules/integration-specs.mjs';
|
|
64
|
+
import { lintSuppressionFindings } from './rules/lint-suppression.mjs';
|
|
65
|
+
import { peerIntegrationFindings } from './rules/peer-integrations.mjs';
|
|
66
|
+
import { pipelineFindings } from './rules/pipeline.mjs';
|
|
67
|
+
import { proofCommandFindings } from './rules/proof-commands.mjs';
|
|
68
|
+
import { repoLocalCheckFindings } from './rules/repo-local-checks.mjs';
|
|
69
|
+
import { readJson } from './rules/read.mjs';
|
|
70
|
+
import { secretFindings } from './rules/secrets.mjs';
|
|
71
|
+
import { pathFindings } from './path-findings.mjs';
|
|
72
|
+
import { onLintSurface } from './architecture/surface.mjs';
|
|
73
|
+
import { checkAppRoot, trackedTreeView } from './architecture/hfs.mjs';
|
|
74
|
+
import { stacksFindings } from './rules/stacks.mjs';
|
|
75
|
+
import { testTopologyFindings } from './rules/test-topology.mjs';
|
|
76
|
+
import { feNoTestsFindings, isFeTestPath } from './rules/fe-no-tests.mjs';
|
|
72
77
|
|
|
73
78
|
export const CANON_PINS_FILE = 'knowledge/hfs/canon-pins.yaml';
|
|
74
79
|
export const FAILURE_CODES_FILE = 'modules/kernel/failure-codes.yaml';
|
|
@@ -86,7 +91,7 @@ export const CHECK_CODES = Object.freeze([
|
|
|
86
91
|
'HFS_SLOT_REQUIRED_MISSING', 'HFS_MIN_INSTANCES', 'HFS_CANON_PIN_DRIFT', 'HFS_SIZE_SOFT_BACKLOG', 'BE_SOURCE_FORM',
|
|
87
92
|
'HFS_MANAGED_FILE_DRIFT', 'HFS_TOOL_CONFIG_LOCAL', 'HFS_RULE_OFF_WITHOUT_REPLACEMENT', 'HFS_TS_STRICT',
|
|
88
93
|
'HFS_PLAINTEXT_SECRET', 'HFS_STACKS_SHAPE', 'HFS_CI_MISSING_CANON', 'HFS_DEP_VERSION_SKEW', 'HFS_CONTRACT_SNAPSHOT_DRIFT',
|
|
89
|
-
'BE_TEST_TOPOLOGY', 'BE_SPEC_PLACEMENT', 'HFS_REPO_LOCAL_CHECK', 'HFS_LINT_SUPPRESSION_FILE', 'HFS_PROOF_COMMAND_FILE_MISSING', 'HFS_PEER_INTEGRATION_MISSING', 'BE_INTEGRATION_SPEC_MISSING', 'FE_NO_TESTS', 'FE_WIRE_GENERATED', 'FE_I18N_PLACEMENT', 'FE_I18N_CATALOG',
|
|
94
|
+
'BE_TEST_TOPOLOGY', 'BE_SPEC_PLACEMENT', 'HFS_REPO_LOCAL_CHECK', 'HFS_LINT_SUPPRESSION_FILE', 'HFS_PROOF_COMMAND_FILE_MISSING', 'HFS_PEER_INTEGRATION_MISSING', 'BE_INTEGRATION_SPEC_MISSING', 'FE_GRAPHQL_CONTRACT', 'FE_NO_TESTS', 'FE_WIRE_GENERATED', 'FE_I18N_PLACEMENT', 'FE_I18N_CATALOG',
|
|
90
95
|
'HFS_GITIGNORE_BLOCK_DRIFT', 'HFS_SONAR_CONFIG', 'HFS_FORMAT',
|
|
91
96
|
'HFS_EMPTY_DIR', 'HFS_GHOST_TREE', 'HFS_UNTRACKED_ROOT_ENTRY',
|
|
92
97
|
...REFUSAL_CODES,
|
|
@@ -253,6 +258,8 @@ function scopeFindings({ repoRoot, root, repo, resolver, files, all = files, sco
|
|
|
253
258
|
const count = instances.filter((i) => i.slot === slot).length;
|
|
254
259
|
if (count < min) findings.push({ code: 'HFS_MIN_INSTANCES', level: 'error', path: resolver.slot(slot).path, slot, min, count, message: `${slot} needs at least ${min} instance${min === 1 ? '' : 's'} (${resolver.slot(slot).path}), found ${count}` });
|
|
255
260
|
}
|
|
261
|
+
// The runtime repository (kind runtime) is judged here by its tree law only; its own rules run in scripts/hfs/runtime-check.mjs.
|
|
262
|
+
if (repo.kind === RUNTIME_KIND) return findings;
|
|
256
263
|
|
|
257
264
|
const pins = readPins(root);
|
|
258
265
|
findings.push(...pinFindings({ repoRoot, files, profile: repo.profile, pins, only: scoped }));
|
|
@@ -268,6 +275,7 @@ function scopeFindings({ repoRoot, root, repo, resolver, files, all = files, sco
|
|
|
268
275
|
...depFindings({ repoRoot, files: all }),
|
|
269
276
|
...peerIntegrationFindings({ repoRoot, files: all }),
|
|
270
277
|
...integrationSpecFindings({ repoRoot, files: all }),
|
|
278
|
+
...feContractFindings({ repoRoot, files: all }),
|
|
271
279
|
...pipelineFindings({ repoRoot, files, pins }),
|
|
272
280
|
...testTopologyFindings({ repoRoot, files }),
|
|
273
281
|
...proofCommandFindings({ repoRoot, files: all, resolver, sides: Object.keys(repo.sides ?? {}) }),
|