@starci/eslint-canon-be 3.0.9 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -1
- package/LICENSE +21 -0
- package/README.md +2 -1
- package/builders.mjs +1 -1
- package/comments.mjs +12 -21
- package/config-owner.mjs +1 -1
- package/idempotency.mjs +54 -5
- package/index.mjs +2 -0
- package/lib/config.mjs +4 -1
- package/lib/doors.mjs +3 -1
- package/module-shape.mjs +1 -2
- package/package.json +3 -2
- package/resilience.mjs +1 -1
- package/runtime/config.example.yaml +187 -0
- package/runtime/engine/by-code-unit.mjs +12 -0
- package/runtime/engine/config.mjs +247 -273
- package/runtime/engine/invalid-config.mjs +5 -5
- package/runtime/engine/model-config.mjs +90 -0
- package/runtime/engine/orca-config.mjs +3 -1
- package/runtime/engine/removed-vocabulary.mjs +28 -0
- package/runtime/engine/resources-config.mjs +52 -0
- package/runtime/engine/runtime-root.mjs +17 -0
- package/runtime/engine/secrets.mjs +134 -0
- package/runtime/engine/sonar-config.mjs +21 -0
- package/runtime/engine/temp-root.mjs +32 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +18 -18
- package/runtime/knowledge/hfs/rules.yaml +102 -69
- package/runtime/knowledge/hfs/slots.yaml +11 -14
- package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
- package/runtime/modules/models/registry.yaml +25 -397
- package/runtime/modules/models/runtimes.yaml +92 -262
- package/runtime/modules/models/tiers.yaml +65 -0
- package/runtime/scripts/api/git/lib.mjs +36 -1
- package/runtime/scripts/hfs/allows.mjs +3 -3
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
- package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
- package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
- package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
- package/runtime/scripts/hfs/architecture/config.mjs +95 -80
- package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
- package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
- package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
- package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
- package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
- package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
- package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
- package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
- package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
- package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
- package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
- package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
- package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
- package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +3 -2
- package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
- package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
- package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
- package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
- package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
- package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
- package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
- package/runtime/scripts/hfs/architecture/index.mjs +158 -102
- package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
- package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
- package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
- package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
- package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
- package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
- package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
- package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
- package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
- package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
- package/runtime/scripts/hfs/architecture/required-files.mjs +100 -68
- package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
- package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
- package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
- package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
- package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
- package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
- package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
- package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
- package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
- package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
- package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
- package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
- package/runtime/scripts/hfs/edition-slots.mjs +5 -3
- package/runtime/scripts/hfs/linear-text.mjs +30 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
- package/runtime/scripts/hfs/path-findings.mjs +58 -38
- package/runtime/scripts/hfs/project-graph.mjs +4 -1
- package/runtime/scripts/hfs/repo-identity.mjs +5 -2
- package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
- package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
- package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
- package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
- package/runtime/scripts/hfs/slot-classify.mjs +65 -0
- package/runtime/scripts/hfs/slot-errors.mjs +11 -0
- package/runtime/scripts/hfs/slot-imports.mjs +76 -0
- package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
- package/runtime/scripts/hfs/slot-match.mjs +153 -0
- package/runtime/scripts/hfs/slot-path.mjs +8 -0
- package/runtime/scripts/hfs/slot-required.mjs +76 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
- package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
- package/runtime/scripts/hfs/slots.mjs +72 -693
- package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
- package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
- package/runtime/scripts/hfs/view.mjs +1 -1
- package/runtime/scripts/lib/dockerfile.mjs +56 -33
- package/runtime/scripts/lib/git.mjs +2 -2
- package/runtime/scripts/lib/i18n.mjs +3 -2
- package/runtime/scripts/lib/jsdoc.mjs +35 -0
- package/runtime/scripts/lib/language.mjs +3 -3
- package/runtime/scripts/lib/list.mjs +8 -1
- package/runtime/scripts/lib/mutation-fence.mjs +15 -0
- package/runtime/scripts/lib/path-key.mjs +42 -9
- package/runtime/scripts/lib/regex.mjs +1 -1
- package/runtime/scripts/lib/secret-patterns.mjs +6 -2
- package/runtime/scripts/lib/sonar-syntax-rules.mjs +225 -0
- package/runtime/scripts/lib/sops-envelope.mjs +103 -2
- package/runtime/scripts/lib/stack-services.mjs +1 -2
- package/runtime/scripts/lib/ts-ast.mjs +9 -0
- package/runtime/scripts/lib/walk.mjs +6 -1
- package/schema-authority.mjs +1 -1
- package/service-deps.mjs +1 -1
- package/sonar-parity.mjs +19 -0
- package/suppression.mjs +2 -6
- package/test-world.mjs +2 -10
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Trailing-slash trimming for slot paths and generated roots, linear in the length of the text.
|
|
2
|
+
|
|
3
|
+
/** `text` without its trailing `/` characters. */
|
|
4
|
+
export function trimTrailingSlashes(text) {
|
|
5
|
+
let end = text.length;
|
|
6
|
+
while (end > 0 && text.codePointAt(end - 1) === 47) end -= 1;
|
|
7
|
+
return text.slice(0, end);
|
|
8
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
-
import { createRequire } from 'node:module';
|
|
3
2
|
import path from 'node:path';
|
|
3
|
+
import { byCodeUnit } from '../lib/list.mjs';
|
|
4
4
|
|
|
5
5
|
// One TypeScript program per (compiler, root names, compiler options, project references) inside a program run, and
|
|
6
6
|
// one value per (kind, compiler, input) for what callers derive from those programs (the architecture context).
|
|
@@ -27,7 +27,7 @@ function keyOf(value) {
|
|
|
27
27
|
}
|
|
28
28
|
if (typeof value !== 'object' || (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)) return null;
|
|
29
29
|
const entries = [];
|
|
30
|
-
for (const name of Object.keys(value).sort()) {
|
|
30
|
+
for (const name of Object.keys(value).sort(byCodeUnit)) {
|
|
31
31
|
const item = keyOf(value[name]);
|
|
32
32
|
if (item === null) return null;
|
|
33
33
|
entries.push(`${JSON.stringify(name)}:${item}`);
|
|
@@ -44,7 +44,10 @@ export function sharedInProgramRun(kind, ts, input, build) {
|
|
|
44
44
|
const key = values ? keyOf(input) : null;
|
|
45
45
|
if (key === null) return build();
|
|
46
46
|
let byKey = values.get(ts);
|
|
47
|
-
if (!byKey)
|
|
47
|
+
if (!byKey) {
|
|
48
|
+
byKey = new Map();
|
|
49
|
+
values.set(ts, byKey);
|
|
50
|
+
}
|
|
48
51
|
const id = `${kind}\0${key}`;
|
|
49
52
|
if (!byKey.has(id)) byKey.set(id, build());
|
|
50
53
|
return byKey.get(id);
|
|
@@ -74,4 +77,3 @@ export function typeScriptProjectReferencePath(ts, reference) {
|
|
|
74
77
|
export function resolveTypeScriptModule(ts, specifier, from, options, host = ts.sys) {
|
|
75
78
|
return ts.resolveModuleName(specifier, from, options, host).resolvedModule?.resolvedFileName;
|
|
76
79
|
}
|
|
77
|
-
|
|
@@ -6,7 +6,7 @@ import path from 'node:path';
|
|
|
6
6
|
import { allowsFile } from './allows.mjs';
|
|
7
7
|
import { openHfs } from './slots.mjs';
|
|
8
8
|
|
|
9
|
-
const posix = (p) => String(p).
|
|
9
|
+
const posix = (p) => String(p).replaceAll('\\', '/');
|
|
10
10
|
const isAbsoluteAny = (text) => path.isAbsolute(text) || /^[A-Za-z]:\//.test(text);
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -58,8 +58,8 @@ export function shellCommands(body) {
|
|
|
58
58
|
} else if (char === '"' || char === "'") {
|
|
59
59
|
quote = char;
|
|
60
60
|
current += char;
|
|
61
|
-
} else if (
|
|
62
|
-
|
|
61
|
+
} else if (isCommandSeparator(body, i, char)) {
|
|
62
|
+
i += commandSeparatorAdvance(body, i, char);
|
|
63
63
|
flush();
|
|
64
64
|
} else if (char === '\\' && body[i + 1] === '\n') {
|
|
65
65
|
i += 1;
|
|
@@ -71,43 +71,17 @@ export function shellCommands(body) {
|
|
|
71
71
|
return commands;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
const isCommandSeparator = (body, index, char) => char === ';' || char === '|' || (char === '&' && body[index + 1] === '&');
|
|
75
|
+
const commandSeparatorAdvance = (body, index, char) => char !== ';' && body[index + 1] === char ? 1 : 0;
|
|
76
|
+
|
|
74
77
|
/**
|
|
75
78
|
* Parse a Dockerfile: { instructions: [{ keyword, text, line, stage }], stages: [{ index, name, image, line, instructions }],
|
|
76
79
|
* comments: [{ text, line }], preamble: [instruction before the first FROM (ARG)] }. `keyword` is upper-case; `text` is the
|
|
77
80
|
* argument text of the logical line; `stage` is the index of the stage the instruction belongs to (-1 before the first FROM).
|
|
78
81
|
*/
|
|
79
82
|
export function parseDockerfile(source) {
|
|
80
|
-
const lines = String(source).
|
|
81
|
-
const instructions =
|
|
82
|
-
const comments = [];
|
|
83
|
-
let escape = '\\';
|
|
84
|
-
let directives = true;
|
|
85
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
86
|
-
const raw = lines[i];
|
|
87
|
-
const trimmed = raw.trim();
|
|
88
|
-
if (trimmed === '') { directives = false; continue; }
|
|
89
|
-
if (trimmed.startsWith('#')) {
|
|
90
|
-
const body = trimmed.slice(1).trim();
|
|
91
|
-
const directive = directives ? /^(syntax|escape|check)\s*=\s*(\S+)$/i.exec(body) : null;
|
|
92
|
-
if (directive) { if (directive[1].toLowerCase() === 'escape') escape = directive[2]; continue; }
|
|
93
|
-
directives = false;
|
|
94
|
-
comments.push({ text: body, line: i + 1 });
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
directives = false;
|
|
98
|
-
const start = i + 1;
|
|
99
|
-
let logical = trimmed;
|
|
100
|
-
while (logical.endsWith(escape) && i + 1 < lines.length) {
|
|
101
|
-
logical = logical.slice(0, -1).trimEnd();
|
|
102
|
-
i += 1;
|
|
103
|
-
while (i < lines.length && lines[i].trim().startsWith('#')) i += 1;
|
|
104
|
-
if (i >= lines.length) break;
|
|
105
|
-
logical = `${logical} ${lines[i].trim()}`;
|
|
106
|
-
}
|
|
107
|
-
const split = logical.search(/\s/);
|
|
108
|
-
const keyword = (split < 0 ? logical : logical.slice(0, split)).toUpperCase();
|
|
109
|
-
instructions.push({ keyword, text: split < 0 ? '' : logical.slice(split).trim(), line: start, stage: -1 });
|
|
110
|
-
}
|
|
83
|
+
const lines = String(source).replaceAll('\r\n', '\n').split('\n');
|
|
84
|
+
const { instructions, comments } = parseInstructions(lines);
|
|
111
85
|
const stages = [];
|
|
112
86
|
const preamble = [];
|
|
113
87
|
for (const instruction of instructions) {
|
|
@@ -122,3 +96,52 @@ export function parseDockerfile(source) {
|
|
|
122
96
|
}
|
|
123
97
|
return { instructions, stages, comments, preamble };
|
|
124
98
|
}
|
|
99
|
+
|
|
100
|
+
function parseInstructions(lines) {
|
|
101
|
+
const instructions = [];
|
|
102
|
+
const comments = [];
|
|
103
|
+
const state = { escape: '\\', directives: true };
|
|
104
|
+
let i = 0;
|
|
105
|
+
while (i < lines.length) {
|
|
106
|
+
const parsed = parseInstructionLine(lines, i, state);
|
|
107
|
+
if (parsed.comment) comments.push(parsed.comment);
|
|
108
|
+
if (parsed.instruction) instructions.push(parsed.instruction);
|
|
109
|
+
i = parsed.nextLine + 1;
|
|
110
|
+
}
|
|
111
|
+
return { instructions, comments };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function parseInstructionLine(lines, index, state) {
|
|
115
|
+
const trimmed = lines[index].trim();
|
|
116
|
+
if (trimmed === '') { state.directives = false; return { nextLine: index }; }
|
|
117
|
+
if (trimmed.startsWith('#')) return parseCommentLine(trimmed, index, state);
|
|
118
|
+
state.directives = false;
|
|
119
|
+
const { logical, nextLine } = logicalInstructionOf(lines, index, state.escape);
|
|
120
|
+
const split = logical.search(/\s/);
|
|
121
|
+
const keyword = (split < 0 ? logical : logical.slice(0, split)).toUpperCase();
|
|
122
|
+
return { nextLine, instruction: { keyword, text: split < 0 ? '' : logical.slice(split).trim(), line: index + 1, stage: -1 } };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function parseCommentLine(trimmed, index, state) {
|
|
126
|
+
const body = trimmed.slice(1).trim();
|
|
127
|
+
const directive = state.directives ? /^(syntax|escape|check)\s*=\s*(\S+)$/i.exec(body) : null;
|
|
128
|
+
if (directive) {
|
|
129
|
+
if (directive[1].toLowerCase() === 'escape') state.escape = directive[2];
|
|
130
|
+
return { nextLine: index };
|
|
131
|
+
}
|
|
132
|
+
state.directives = false;
|
|
133
|
+
return { nextLine: index, comment: { text: body, line: index + 1 } };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function logicalInstructionOf(lines, index, escape) {
|
|
137
|
+
let logical = lines[index].trim();
|
|
138
|
+
let nextLine = index;
|
|
139
|
+
while (logical.endsWith(escape) && nextLine + 1 < lines.length) {
|
|
140
|
+
logical = logical.slice(0, -1).trimEnd();
|
|
141
|
+
nextLine += 1;
|
|
142
|
+
while (nextLine < lines.length && lines[nextLine].trim().startsWith('#')) nextLine += 1;
|
|
143
|
+
if (nextLine >= lines.length) break;
|
|
144
|
+
logical = `${logical} ${lines[nextLine].trim()}`;
|
|
145
|
+
}
|
|
146
|
+
return { logical, nextLine };
|
|
147
|
+
}
|
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export const unquoteDiffPath = (value) =>
|
|
9
9
|
/^".*"$/.test(value)
|
|
10
|
-
? JSON.parse(value.replace(/\\([0-7]{3})/g, (_, octal) =>
|
|
10
|
+
? JSON.parse(value.replace(/\\([0-7]{3})/g, (_, octal) => String.raw`\u00${Number.parseInt(octal, 8).toString(16).padStart(2, '0')}`))
|
|
11
11
|
: value;
|
|
12
12
|
|
|
13
13
|
// git's repository-local variables (git rev-parse --local-env-vars). A hook or alias run in a linked worktree exports
|
|
14
14
|
// GIT_DIR=<main>/.git/worktrees/<wt>; a spec inheriting it pointed every fixture git at the LIVE .claude repo and re-inited
|
|
15
15
|
// it core.bare=true (2026-09-29, a6f60352c). tests/setup/isolated-registry.mjs drops the same list.
|
|
16
|
-
export const GIT_LOCAL_ENV_VARS = ['GIT_DIR', 'GIT_COMMON_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_OBJECT_DIRECTORY', 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_IMPLICIT_WORK_TREE', 'GIT_PREFIX', 'GIT_CONFIG', 'GIT_CONFIG_PARAMETERS', 'GIT_CONFIG_COUNT', 'GIT_GRAFT_FILE', 'GIT_NO_REPLACE_OBJECTS', 'GIT_REPLACE_REF_BASE', 'GIT_SHALLOW_FILE'];
|
|
16
|
+
export const GIT_LOCAL_ENV_VARS = ['GIT_DIR', 'GIT_COMMON_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_OBJECT_DIRECTORY', 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_IMPLICIT_WORK_TREE', 'GIT_PREFIX', 'GIT_CONFIG', 'GIT_CONFIG_PARAMETERS', 'GIT_CONFIG_COUNT', 'GIT_GRAFT_FILE', 'GIT_NO_REPLACE_OBJECTS', 'GIT_REPLACE_REF_BASE', 'GIT_SHALLOW_FILE', 'GIT_INTERNAL_SUPER_PREFIX'];
|
|
17
17
|
/** `parent` (the caller's env) without git's repository-local variables (GIT_CONFIG_KEY_n/VALUE_n go with GIT_CONFIG_COUNT). Pure. */
|
|
18
18
|
export function withoutGitLocalEnv(parent) {
|
|
19
19
|
const env = { ...parent };
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// A placeholder is `{name}`; the English and the Vietnamese of one entry carry exactly the same placeholders
|
|
12
12
|
// (scripts/checks/check-i18n-catalog.mjs). An English source with no entry is returned as is: a missing translation degrades
|
|
13
13
|
// to English, never to a blank.
|
|
14
|
+
import { byCodeUnit } from './list.mjs';
|
|
14
15
|
import { escapeRegExp } from './regex.mjs';
|
|
15
16
|
import fs from 'node:fs';
|
|
16
17
|
import path from 'node:path';
|
|
@@ -19,7 +20,7 @@ import { parseYaml } from '../../engine/yaml.mjs';
|
|
|
19
20
|
import { DEFAULT_OWNER_LANGUAGE, loadConfig } from '../../engine/config.mjs';
|
|
20
21
|
|
|
21
22
|
export const CATALOG_DIR = 'modules/i18n/messages';
|
|
22
|
-
export const PLACEHOLDER = /\{([A-Za-z_]
|
|
23
|
+
export const PLACEHOLDER = /\{([A-Za-z_]\w*)\}/g;
|
|
23
24
|
|
|
24
25
|
const cache = new Map();
|
|
25
26
|
|
|
@@ -54,7 +55,7 @@ export const resetCatalogCache = () => cache.clear();
|
|
|
54
55
|
/** The names each `pattern` match captures (group 1) in `text`, in match order (`sort` sorts them). */
|
|
55
56
|
export const captureNames = (text, pattern, { sort = false } = {}) => {
|
|
56
57
|
const names = [...String(text).matchAll(pattern)].map((m) => m[1]);
|
|
57
|
-
return sort ? names.sort() : names;
|
|
58
|
+
return sort ? names.sort(byCodeUnit) : names;
|
|
58
59
|
};
|
|
59
60
|
|
|
60
61
|
/** The names of the placeholders of a message. */
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The description form shared by runtime and product public-documentation checks.
|
|
3
|
+
* This proves local form; responsibility, accuracy and important decision rationale remain review.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Whether AST-selected description text contains a letter or number; language and meaning remain review. */
|
|
7
|
+
export const hasJsdocDescription = (description) => typeof description === "string" && /[\p{L}\p{N}]/u.test(description);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Read description text before block tags; file-level headers do not describe a declaration.
|
|
11
|
+
* Tag payloads and punctuation cannot make an empty API description pass.
|
|
12
|
+
*/
|
|
13
|
+
export const jsdocDescription = (comment) => {
|
|
14
|
+
if (comment?.type !== "Block" || !comment.value.startsWith("*")) return "";
|
|
15
|
+
const prose = comment.value.replace(/^\*/, "").split(/\r?\n/u)
|
|
16
|
+
.map((line) => line.replace(/^\s*\*(?: ?)/u, "")).join("\n");
|
|
17
|
+
if (/(?:^|\s)@(?:file|fileoverview|overview|license|copyright)\b/u.test(prose)) return "";
|
|
18
|
+
const description = prose.split(/(?:^|\s)@[A-Za-z][\w-]*(?=\s|$)/u, 1)[0].trim();
|
|
19
|
+
return hasJsdocDescription(description) ? description : "";
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Return the final adjacent JSDoc only when it describes this declaration.
|
|
24
|
+
* Decorated classes document the export after its decorators; member docs precede their decorators.
|
|
25
|
+
*/
|
|
26
|
+
export const jsdocBefore = (sourceCode, node) => {
|
|
27
|
+
const exported = node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration";
|
|
28
|
+
const anchor = exported
|
|
29
|
+
? sourceCode.getTokens(node).find((token) => token.type === "Keyword" && token.value === "export")
|
|
30
|
+
: node.decorators?.[0] ?? node;
|
|
31
|
+
if (!anchor) return null;
|
|
32
|
+
const comment = sourceCode.getCommentsBefore(anchor).at(-1);
|
|
33
|
+
if (!jsdocDescription(comment)) return null;
|
|
34
|
+
return /^\s*$/u.test(sourceCode.getText().slice(comment.range[1], anchor.range[0])) ? comment : null;
|
|
35
|
+
};
|
|
@@ -50,13 +50,13 @@ export const DECLARED_VIETNAMESE_FIELDS = Object.freeze({
|
|
|
50
50
|
'modules/ops/_labels.yaml': Object.freeze({ flowFields: Object.freeze(['vi']) }),
|
|
51
51
|
'modules/goal/archetypes.yaml': Object.freeze({
|
|
52
52
|
fields: Object.freeze([
|
|
53
|
-
'buildIntent', 'canonIntent', 'e2eIntent', 'uatIntent', 'proofNegation', '
|
|
53
|
+
'buildIntent', 'canonIntent', 'e2eIntent', 'uatIntent', 'proofNegation', 'proofStateCue', 'integrationIntent', 'brandIntent',
|
|
54
54
|
'phrases', 'requires', 'excludes', 'backend', 'frontend', 'package',
|
|
55
55
|
]),
|
|
56
56
|
}),
|
|
57
57
|
'modules/goal/source-phrases.yaml': Object.freeze({
|
|
58
58
|
fields: Object.freeze([
|
|
59
|
-
'prefer', 'avoid', 'negation', 'refused', 'accept', 'golden',
|
|
59
|
+
'prefer', 'avoid', 'only', 'negation', 'refused', 'accept', 'golden',
|
|
60
60
|
'missingPaths', 'grantTooNarrow', 'toolTimeout', 'testGap', 'checkerUnavailable',
|
|
61
61
|
'supervisorVerbs', 'ownerWord', 'orWord', 'beyondAuthority', 'ownerOnly', 'workerDied', 'contractConflict', 'decision',
|
|
62
62
|
'product', 'grammar', 'knowledge', 'sourceLabel', 'internalVocabulary', 'actor', 'success',
|
|
@@ -124,7 +124,7 @@ export function documentLanguageHits(rel, text) {
|
|
|
124
124
|
if (declared.fields?.includes(keys[hit.line - 1])) return false;
|
|
125
125
|
if (!declared.flowFields) return true;
|
|
126
126
|
// an inline flow map: drop the declared fields' values and judge what is left of the line
|
|
127
|
-
const rest = declared.flowFields.reduce((line, field) => line.replace(new RegExp(
|
|
127
|
+
const rest = declared.flowFields.reduce((line, field) => line.replace(new RegExp(String.raw`\b${field}\s*:\s*(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^,}]*)`, 'g'), ''), lines[hit.line - 1]);
|
|
128
128
|
return hasSecondLanguage(rest);
|
|
129
129
|
});
|
|
130
130
|
}
|
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
export const list = (v) => (Array.isArray(v) ? v : []);
|
|
5
5
|
|
|
6
6
|
/** `v` as an array: itself when already one, [] for null/undefined, else [v]. */
|
|
7
|
-
export const asList = (v) =>
|
|
7
|
+
export const asList = (v) => {
|
|
8
|
+
if (Array.isArray(v)) return v;
|
|
9
|
+
if (v == null) return [];
|
|
10
|
+
return [v];
|
|
11
|
+
};
|
|
8
12
|
|
|
9
13
|
/** The object entries of `v` (a non-array reads as []). */
|
|
10
14
|
export const objectList = (v) => (Array.isArray(v) ? v : []).filter((item) => item && typeof item === 'object');
|
|
@@ -18,3 +22,6 @@ export const splitList = (v, { sep = ',', dedupe = false } = {}) => {
|
|
|
18
22
|
const items = String(v ?? '').split(sep).map((s) => s.trim()).filter(Boolean);
|
|
19
23
|
return dedupe ? [...new Set(items)] : items;
|
|
20
24
|
};
|
|
25
|
+
|
|
26
|
+
// The comparator of a bare `sort()` (UTF-16 code-unit order) is owned by the engine, which cannot import scripts/lib.
|
|
27
|
+
export { byCodeUnit } from '../../engine/by-code-unit.mjs';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// mutation-fence.mjs — ephemeral admission carried through awaited work, never a state store.
|
|
2
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
|
+
const scope = new AsyncLocalStorage();
|
|
4
|
+
|
|
5
|
+
/** Run one admitted call; the checker is synchronous and reads its existing durable owners again. */
|
|
6
|
+
export const withMutationFence = (check, authority, fn) => scope.run({ check, authority, checking: false }, fn);
|
|
7
|
+
/** Recheck after the actual transaction lock or immediately before a native effect. */
|
|
8
|
+
export function assertMutationFence(boundary) {
|
|
9
|
+
const current = scope.getStore();
|
|
10
|
+
if (!current || current.checking) return;
|
|
11
|
+
current.checking = true;
|
|
12
|
+
try { current.check(boundary); } finally { current.checking = false; }
|
|
13
|
+
}
|
|
14
|
+
/** Identity stamped by existing write owners, never accepted from a command argument. */
|
|
15
|
+
export const mutationAuthority = () => scope.getStore()?.authority ?? null;
|
|
@@ -6,6 +6,17 @@ const WIN = process.platform === 'win32';
|
|
|
6
6
|
|
|
7
7
|
/** `p` with every backslash turned into a forward slash; null and undefined read as ''. */
|
|
8
8
|
export const slash = (p) => String(p ?? '').replaceAll('\\', '/');
|
|
9
|
+
/** `text` without its trailing '/' run (a loop: a trailing-run pattern backtracks super-linearly). */
|
|
10
|
+
export const trimTrailingSlashes = (text) => {
|
|
11
|
+
for (let end = text.length; ; end -= 1) if (end === 0 || text[end - 1] !== '/') return text.slice(0, end);
|
|
12
|
+
};
|
|
13
|
+
// A trailing '*' run with the one '/' before it (the glob suffix of 'dir/**'); text without a trailing '*' is unchanged.
|
|
14
|
+
const stripTrailingStars = (text) => {
|
|
15
|
+
let end = text.length;
|
|
16
|
+
while (end > 0 && text[end - 1] === '*') end -= 1;
|
|
17
|
+
if (end === text.length) return text;
|
|
18
|
+
return text.slice(0, end > 0 && text[end - 1] === '/' ? end - 1 : end);
|
|
19
|
+
};
|
|
9
20
|
/** A relative path in git's spelling: forward slashes, no leading './'. */
|
|
10
21
|
export const posixPath = (p) => slash(p).replace(/^\.\//, '');
|
|
11
22
|
/** Whether posix `file` is `prefix` itself or under it (`file === prefix || file` starts with `prefix/`). */
|
|
@@ -19,7 +30,7 @@ export const samePath = (a, b) => foldCase(a) === foldCase(b);
|
|
|
19
30
|
* `fold: true` folds case on every platform (a comparison against a lowercase-stored root needs it).
|
|
20
31
|
*/
|
|
21
32
|
export const pathKey = (p, { fold = WIN } = {}) => {
|
|
22
|
-
const key = slash(path.resolve(p))
|
|
33
|
+
const key = trimTrailingSlashes(slash(path.resolve(p)));
|
|
23
34
|
return fold ? key.toLowerCase() : key;
|
|
24
35
|
};
|
|
25
36
|
/** `a` is `b` or under it, or the reverse: two path spellings whose trees touch. */
|
|
@@ -36,37 +47,59 @@ export const normPath = (p, { trim = false, collapse = false, dot = false, glob
|
|
|
36
47
|
s = slash(s);
|
|
37
48
|
if (collapse) s = s.replace(/\/{2,}/g, '/');
|
|
38
49
|
if (dot) s = s.replace(/^\.\//, '');
|
|
39
|
-
if (glob === 'star') s = s
|
|
50
|
+
if (glob === 'star') s = stripTrailingStars(s);
|
|
40
51
|
else if (glob === 'double') s = s.replace(/\/\*\*$/, '');
|
|
41
|
-
s = s
|
|
52
|
+
s = trimTrailingSlashes(s);
|
|
42
53
|
return fold ? s.toLowerCase() : s;
|
|
43
54
|
};
|
|
44
55
|
/** A work-relative path in one spelling: forward slashes, no leading ./, no trailing slash. */
|
|
45
|
-
export const normWork = (p) => String(p ?? '').trim().replaceAll('\\', '/').replace(/^\.\/+/, '')
|
|
56
|
+
export const normWork = (p) => trimTrailingSlashes(String(p ?? '').trim().replaceAll('\\', '/').replace(/^\.\/+/, ''));
|
|
46
57
|
|
|
47
58
|
/** `p` resolved and dereferenced (realpath where the path exists) — the canonical spelling of a checkout dir. */
|
|
48
59
|
export const realPath = (p) => {
|
|
49
60
|
const resolved = path.resolve(p);
|
|
50
61
|
try { return fs.realpathSync.native(resolved); } catch { return resolved; }
|
|
51
62
|
};
|
|
63
|
+
/** `p` with its nearest existing ancestor dereferenced and the not-yet-created rest appended: a root's canonical spelling before it exists. */
|
|
64
|
+
export const canonicalPath = (p) => {
|
|
65
|
+
const rest = [];
|
|
66
|
+
for (let at = path.resolve(p); ; at = path.dirname(at)) {
|
|
67
|
+
try { return path.join(fs.realpathSync.native(at), ...rest); } catch { /* not created yet */ }
|
|
68
|
+
if (path.dirname(at) === at) return path.resolve(p);
|
|
69
|
+
rest.unshift(path.basename(at));
|
|
70
|
+
}
|
|
71
|
+
};
|
|
52
72
|
/** `p` resolved and case-folded where the filesystem ignores case — the Map key of an absolute path. */
|
|
53
73
|
export const resolvedKey = (p) => foldCase(path.resolve(p));
|
|
54
74
|
/** Whether `a` and `b` resolve to the same path (always case-folded, so differently-cased spellings count). */
|
|
55
75
|
export const sameResolvedPath = (a, b) => pathKey(a, { fold: true }) === pathKey(b, { fold: true });
|
|
56
76
|
/**
|
|
57
|
-
* Whether `file` sits
|
|
58
|
-
* realpath-folded treeKey) when
|
|
77
|
+
* Whether `file` sits under `root`; `includeSelf` also admits the root itself. `key` normalizes
|
|
78
|
+
* both sides first (e.g. the registry's realpath-folded treeKey) when their spellings need it.
|
|
59
79
|
*/
|
|
60
|
-
export const insidePath = (root, file, { key = (p) => p } = {}) => {
|
|
80
|
+
export const insidePath = (root, file, { key = (p) => p, includeSelf = false } = {}) => {
|
|
61
81
|
const rel = path.relative(key(root), key(file));
|
|
62
|
-
return Boolean(rel) && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
82
|
+
return (includeSelf || Boolean(rel)) && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* `candidate` (absolute, or relative to `base`) resolved and dereferenced, required to sit under `base` (or be it):
|
|
86
|
+
* the boundary check for a path an operator, an agent or another process hands in. `base` is canonicalised once and the
|
|
87
|
+
* candidate is judged below it, so `..` segments and links leading out are refused; returns the canonical path, or throws
|
|
88
|
+
* a PATH_OUTSIDE_BASE refusal (`label` names the argument in the message).
|
|
89
|
+
*/
|
|
90
|
+
export const containedPath = (base, candidate, { label = 'path' } = {}) => {
|
|
91
|
+
const root = canonicalPath(base);
|
|
92
|
+
const resolved = canonicalPath(path.resolve(root, String(candidate ?? '')));
|
|
93
|
+
const prefix = root.endsWith(path.sep) ? root : root + path.sep;
|
|
94
|
+
if (!insidePath(root, resolved, { includeSelf: true }) || (resolved !== root && !resolved.startsWith(prefix))) throw Object.assign(new Error(`${label} ${String(candidate)} is outside ${root} [PATH_OUTSIDE_BASE]`), { code: 'PATH_OUTSIDE_BASE' });
|
|
95
|
+
return resolved;
|
|
63
96
|
};
|
|
64
97
|
/**
|
|
65
98
|
* A path-ish value (a string or a {path} entry) in relative compare form: trimmed, forward slashes,
|
|
66
99
|
* no trailing '/**' glob or slashes, no leading './'; `fold` lowercases for a case-insensitive key.
|
|
67
100
|
*/
|
|
68
101
|
export const normRel = (p, { fold = false } = {}) => {
|
|
69
|
-
const s = String(typeof p === 'string' ? p : p?.path ?? '').trim().
|
|
102
|
+
const s = trimTrailingSlashes(String(typeof p === 'string' ? p : p?.path ?? '').trim().replaceAll('\\', '/').replace(/\/\*\*$/, '')).replace(/^\.\//, '');
|
|
70
103
|
return fold ? s.toLowerCase() : s;
|
|
71
104
|
};
|
|
72
105
|
/** Whether `file` equals or sits under one of `prefixes` (posix spellings; `dot` counts a '.' prefix as covering all). */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// regex.mjs - building a RegExp from text.
|
|
2
2
|
|
|
3
3
|
/** `text` with every regular-expression metacharacter escaped, so it matches itself literally. */
|
|
4
|
-
export const escapeRegExp = (text) => String(text).replace(/[.*+?^${}()|[\]\\]/g,
|
|
4
|
+
export const escapeRegExp = (text) => String(text).replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
|
@@ -15,7 +15,7 @@ export const FORBIDDEN_FILES = [
|
|
|
15
15
|
export const SECRET_PATTERNS = [
|
|
16
16
|
{ name: 'private-key-block', re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/ },
|
|
17
17
|
{ name: 'aws-access-key', re: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
18
|
-
{ name: 'github-token', re: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b|\bgithub_pat_
|
|
18
|
+
{ name: 'github-token', re: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b|\bgithub_pat_\w{50,}\b/ },
|
|
19
19
|
{ name: 'anthropic-key', re: /\bsk-ant-[A-Za-z0-9_-]{20,}/ },
|
|
20
20
|
{ name: 'openai-key', re: /\bsk-(?:proj-)?[A-Za-z0-9]{32,}\b/ },
|
|
21
21
|
{ name: 'slack-token', re: /\bxox[baprs]-[A-Za-z0-9-]{10,}/ },
|
|
@@ -28,7 +28,11 @@ export const SECRET_PATTERNS = [
|
|
|
28
28
|
// candidate; only this heuristic takes the exemption, never a provider-shaped token above.
|
|
29
29
|
// A spec file's keyword-assigned values are test inputs (a spec's sign-in
|
|
30
30
|
// form password and a mocked accessToken once refused a push): the heuristic skips spec files too.
|
|
31
|
-
{ name: 'assigned-secret', re:
|
|
31
|
+
{ name: 'assigned-secret', re: new RegExp([
|
|
32
|
+
String.raw`\b(?:password|passwd|secret|api[_-]?key|access[_-]?token|client[_-]?secret)`,
|
|
33
|
+
String.raw`\b\s*[:=]\s*['"](?!env\([A-Z_][A-Z0-9_]*\)['"])([^'"\s$<{]{12,})`,
|
|
34
|
+
`['"]`,
|
|
35
|
+
].join(''), 'i'), placeholder: /fixture|stub|fake|dummy|placeholder|example|sample|changeme|redacted|mock/i,
|
|
32
36
|
skipFile: /\.(?:spec|test|e2e-spec)\.[cm]?[jt]sx?$/ },
|
|
33
37
|
];
|
|
34
38
|
|