@starci/hfs 4.7.11 → 4.7.12

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 CHANGED
@@ -1,5 +1,8 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.7.12 - 2026-10-19
4
+ - Fixed: bundled source analysis uses explicit nullable contracts, unary callback arguments and grouped regular-expression alternatives while preserving refusal paths; canon pins follow the matching package cohort.
5
+
3
6
  ## 4.7.11 - 2026-10-19
4
7
  - Changed: app release and Sonar adapters use native target binding, with local database migration and Docker transport preserving the declared stack target.
5
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starci/hfs",
3
- "version": "4.7.11",
3
+ "version": "4.7.12",
4
4
  "description": "The StarCi app command implementation for scaffolding, linting, checking and synchronizing one product repository.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,16 @@
1
+ // digest.mjs — the one SHA-256 the runtime hashes with. A leaf: every file's integrity digest,
2
+ // every ledger input ref and every stamped asset is this function, so a byte stream and a file
3
+ // never disagree about what their digest is.
4
+ import { createHash } from 'node:crypto';
5
+ import fs from 'node:fs';
6
+
7
+ /** The lowercase hex SHA-256 of `value` (string, Buffer or TypedArray). */
8
+ export const sha256 = (value) => createHash('sha256').update(value).digest('hex');
9
+ /** The SHA-256 of the file's exact bytes; throws when the file cannot be read. */
10
+ export function sha256File(file){
11
+ // A numeric descriptor belongs to the caller; positional reads preserve its offset.
12
+ const owned=typeof file!=='number',fd=owned?fs.openSync(file,'r'):file;
13
+ const hash=createHash('sha256'),buffer=Buffer.alloc(1024*1024);
14
+ try{let at=0,n;while((n=fs.readSync(fd,buffer,0,buffer.length,at))>0){hash.update(buffer.subarray(0,n));at+=n;}return hash.digest('hex');}
15
+ finally{if(owned)fs.closeSync(fd);}
16
+ }
@@ -29,14 +29,14 @@ pins:
29
29
  source: packages/grammar/package.json
30
30
  why: 'the grammar front ends read: the brand layer sets `--font-sans` and `--font-mono` and the grammar reads them; it carries the Input tel kind, the copyable and read-only Input, the IconButton disclosure props and count badge, ChatBubble, FillViewport, Textarea mentions and Enter-to-send, the leading PrimaryRailLayout rail, the plate-less Image, ListRow, the soft Alert, Progress steps, the floating primary IconButton, FlowGraph, the STATE-8 loading views and the bordered selectable TagGroup chips. The runtime source version wins for a @starci package.'
31
31
  '@starci/eslint-canon-be':
32
- version: 3.4.17
32
+ version: 3.4.18
33
33
  group: starci
34
34
  install: registry
35
35
  side: be
36
36
  source: packages/eslint/be/package.json
37
37
  why: 'the back-end ESLint canon: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; `starciBeConfig({ hfs: loadHfs(import.meta.url) })` is the typed factory and the BE-CONVENTION laws are its rules.'
38
38
  '@starci/eslint-canon-fe':
39
- version: 8.4.17
39
+ version: 8.4.18
40
40
  group: starci
41
41
  install: registry
42
42
  side: fe
@@ -76,14 +76,14 @@ pins:
76
76
  source: packages/test-world/package.json
77
77
  why: 'the shared e2e library of every back end (R47, R48), a devDependency of every back end: the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs and the outage lock. Kafka is real own infra (the one digest-pinned apache/kafka KRaft image, a broker listener and proxy per slot, slot-prefixed topics, consumer groups and client ids) and Postgres is schema-per-context (a schema and a login role per context in a shared database). Each worker has its own data slot (<ns>_w<k>) with its own databases, Keycloak realm, leased Redis DB, bucket/collection/topic prefixes, fakes host, toxiproxy proxies and outage lock; a run provisions min(jest workers, workers cap, default 2) slots, and state file protocol 2 pairs exactly with @starci/jest-preset (TEST_WORLD_PAIR_MISMATCH otherwise). A modules world boots real peer apps beside its modules, world.apps.<name>.during(fn) is the outage of a peer app, w.keycloak.clientSecret(client) answers the run-generated secret of a confidential realm client, world.keycloak.events/sessions read the realm user events and live sessions, world.infra.postgresql.connection(name) takes one database connection down while the others serve, and world.resolve and scope.resolve take any Nest token. Every path a declaration names (`stack`, seeds, the realm, Dockerfiles) resolves from the app root (the directory of hfs.json); the globalSetup registers the path aliases as TypeScript resolves them. Its CLI main is exported at ./cli for starci app stack.'
78
78
  '@starci/cli':
79
- version: 1.7.11
79
+ version: 1.7.12
80
80
  group: starci
81
81
  install: registry
82
82
  side: both
83
83
  source: packages/cli/package.json
84
84
  why: 'the only StarCi command package; product repositories install it for starci app, while @starci/hfs remains its pinned transitive implementation.'
85
85
  '@starci/hfs':
86
- version: 4.7.11
86
+ version: 4.7.12
87
87
  group: starci
88
88
  install: registry
89
89
  side: both
@@ -1,7 +1,7 @@
1
1
  // GENERATED by scripts/cli/gen-catalog.mjs — do not edit
2
2
  export const CATALOG = {
3
3
  "schema": "starci/cli-catalog@1",
4
- "hash": "c357c5a34782ec5039dba046ba98d9d3342881fb600501dd1b9d86bad5fb2cd2",
4
+ "hash": "8913c2eedad972f96410ed412622ca511f32f9a4f2b5fb8104b745e310007ac5",
5
5
  "global": [
6
6
  {
7
7
  "name": "json",
@@ -10894,7 +10894,8 @@ export const CATALOG = {
10894
10894
  "starci supabase start --cwd <app>"
10895
10895
  ],
10896
10896
  "editions": [
10897
- "full"
10897
+ "full",
10898
+ "lite"
10898
10899
  ],
10899
10900
  "impl": {
10900
10901
  "module": "scripts/machine/supabase-stack.mjs",
@@ -10931,7 +10932,8 @@ export const CATALOG = {
10931
10932
  "starci supabase status --cwd <app> --json"
10932
10933
  ],
10933
10934
  "editions": [
10934
- "full"
10935
+ "full",
10936
+ "lite"
10935
10937
  ],
10936
10938
  "impl": {
10937
10939
  "module": "scripts/machine/supabase-stack.mjs",
@@ -10973,7 +10975,8 @@ export const CATALOG = {
10973
10975
  "starci supabase stop --cwd <app> --no-backup"
10974
10976
  ],
10975
10977
  "editions": [
10976
- "full"
10978
+ "full",
10979
+ "lite"
10977
10980
  ],
10978
10981
  "impl": {
10979
10982
  "module": "scripts/machine/supabase-stack.mjs",
@@ -15,6 +15,7 @@ import { realpathOr } from '../../lib/fs-kind.mjs';
15
15
  export function isLinkLike(p, { parentReal = null, stat = null } = {}) {
16
16
  let st = stat;
17
17
  if (!st) { try { st = fs.lstatSync(p); } catch { return false; } }
18
+ if (!st) return false;
18
19
  if (st.isSymbolicLink()) return true;
19
20
  if (!st.isDirectory()) return false;
20
21
  try { fs.readlinkSync(p); return true; } catch { /* not a link readlink can read */ }
@@ -104,7 +104,11 @@ export const SELECTED_AGE_VERSIONS = new Set(['1.2.1', '1.3.1']);
104
104
  const IDENTITY_TIMEOUT_MS = 20_000;
105
105
  const CONTROLLED_ENV = new Set(['SOPS_AGE_KEY', 'SOPS_AGE_KEY_FILE', 'SOPS_AGE_KEY_CMD', 'SOPS_AGE_SSH_PRIVATE_KEY_FILE', 'SOPS_AGE_SSH_PRIVATE_KEY_CMD', 'SOPS_AGE_RECIPIENT', 'SOPS_KEYSERVICE', 'SOPS_ENABLE_LOCAL_KEYSERVICE', 'SOPS_DECRYPTION_ORDER', 'SOPS_KMS_ARN', 'SOPS_GCP_KMS_IDS', 'SOPS_HUAWEICLOUD_KMS_IDS', 'SOPS_AZURE_KEYVAULT_URLS', 'SOPS_VAULT_URIS', 'SOPS_PGP_FP', 'SOPS_AGE_RECIPIENTS', 'HOME', 'USERPROFILE', 'APPDATA', 'XDG_CONFIG_HOME']);
106
106
 
107
- /** Construct only the child map; empty desktop roots make Go return directory errors, not fallback paths. */
107
+ /**
108
+ * Construct only the child map; empty desktop roots make Go return directory errors, not fallback paths.
109
+ * @param {object} env The admitted caller environment.
110
+ * @param {string|null} inlineName Selected inline identity name, or null to omit every identity.
111
+ */
108
112
  function isolatedSopsEnv(env, inlineName = 'SOPS_AGE_KEY') {
109
113
  const out = Object.fromEntries(Object.entries(env).filter(([name]) => !CONTROLLED_ENV.has(name.toUpperCase())));
110
114
  out.HOME = ''; out.USERPROFILE = ''; out.APPDATA = ''; out.XDG_CONFIG_HOME = '';
@@ -65,7 +65,7 @@ function templatePatternOf(node) {
65
65
  return [node.head.text, ...node.templateSpans.map(span => span.literal.text)].join('\u0000').split('.').map(segment => {
66
66
  if (!segment.includes('\u0000')) return segment;
67
67
  const parts = segment.split('\u0000');
68
- return parts.every(part => part === '') ? '*' : new RegExp(`^${parts.map(escapeRegExp).join('.*')}$`, 'su');
68
+ return parts.every(part => part === '') ? '*' : new RegExp(`^${parts.map((part) => escapeRegExp(part)).join('.*')}$`, 'su');
69
69
  });
70
70
  }
71
71
 
@@ -72,8 +72,7 @@ function stronglyConnected(adjacency) {
72
72
  for (const start of adjacency.keys()) {
73
73
  if (state.index.has(start)) continue;
74
74
  startTraversal(state, adjacency, start);
75
- while (state.work.length) {
76
- const frame = state.work.at(-1);
75
+ for (let frame = state.work.at(-1); frame; frame = state.work.at(-1)) {
77
76
  const next = frame.iterator.next();
78
77
  if (next.done) {
79
78
  finishFrame(state, frame);
@@ -1,7 +1,7 @@
1
1
  // app db plan: compare migration-owned state without fetching commands or Vault values.
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { createHash } from 'node:crypto';
4
+ import { sha256 } from '../../engine/digest.mjs';
5
5
  import pg from 'libpg-query';
6
6
  import { parse as parseToml } from 'smol-toml';
7
7
  import { invocationDir } from '../lib/roots.mjs';
@@ -19,12 +19,11 @@ const PRESENCE_SQL = `select
19
19
  pg_catalog.to_regclass('vault.secrets') is not null as vault;`;
20
20
  const INVENTORIES = {
21
21
  migrations: 'select version from supabase_migrations.schema_migrations order by version;',
22
- cron: 'select jobname as name, schedule, pg_catalog.md5(command) as "commandHash", active from cron.job order by jobid;',
22
+ cron: 'select jobname as name, schedule, pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(command, \'UTF8\')), \'hex\') as "commandHash", active from cron.job order by jobid;',
23
23
  queues: 'select queue_name as name from pgmq.meta order by queue_name;',
24
24
  vault: 'select name from vault.secrets order by name;',
25
25
  };
26
26
  const names = (nodes) => sqlStringParts(nodes).join('.');
27
- const hash = (command) => createHash('md5').update(command).digest('hex');
28
27
  const OWNERSHIP_CALLS = new Set(['pgmq.create', 'pgmq.drop_queue', 'cron.schedule', 'cron.unschedule', 'cron.schedule_in_database', 'cron.alter_job']);
29
28
 
30
29
  const parseSql = sqlParser(pg, () => { throw new DbVerbError('migration SQL could not be inventoried safely'); });
@@ -45,7 +44,7 @@ function inventoryCall(state, call) {
45
44
  const job = requiredLiteral(call, 0, ['job_name']);
46
45
  const schedule = requiredLiteral(call, 1, ['schedule']);
47
46
  const command = requiredLiteral(call, 2, ['command']);
48
- state.cron.set(job, { schedule, commandHash: hash(command), active: true });
47
+ state.cron.set(job, { schedule, commandHash: sha256(command), active: true });
49
48
  } else if (name === 'cron.unschedule') {
50
49
  state.cron.delete(requiredLiteral(call, 0, ['job_name']));
51
50
  } else if (name === 'cron.schedule_in_database' || name === 'cron.alter_job') {
@@ -224,7 +223,7 @@ function compare(expected, actual, kind) {
224
223
  for (const row of actual) {
225
224
  const key = kind === 'migrations' ? row.version : row.name;
226
225
  if (key !== null && (typeof key !== 'string' || key.length === 0)) throw new DbVerbError('Supabase returned an invalid inventory identifier');
227
- if (kind === 'cron' && (typeof row.schedule !== 'string' || !/^[a-f0-9]{32}$/.test(row.commandHash ?? '') || typeof row.active !== 'boolean')) throw new DbVerbError('Supabase returned invalid cron metadata');
226
+ if (kind === 'cron' && (typeof row.schedule !== 'string' || !/^[a-f0-9]{64}$/.test(row.commandHash ?? '') || typeof row.active !== 'boolean')) throw new DbVerbError('Supabase returned invalid cron metadata');
228
227
  if (!unmatched.delete(key)) unexpected += 1;
229
228
  else if (kind === 'cron') {
230
229
  const desired = expected.get(key);
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import { skillRoot } from '../../engine/runtime-root.mjs';
5
5
  import { parseYamlCached } from '../lib/yaml-cached.mjs';
6
6
  import { jsonClone } from '../lib/json-clone.mjs';
7
+ import { byCodeUnit } from '../lib/list.mjs';
7
8
  import { isPlainObject } from '../../engine/plain-object.mjs';
8
9
  import { SEMVER } from './manifest-shape.mjs';
9
10
  import { enforcerJudgedInEdition, judgedInEdition, ruleEditionProblems } from './edition-slots.mjs';
@@ -39,7 +40,7 @@ function relativePathProblem(value, label, bad, anchor = false) {
39
40
  const parts = typeof value === 'string' ? value.split('#') : [];
40
41
  const file = parts[0];
41
42
  const valid = typeof value === 'string' && value.trim() === value && value.length > 0
42
- && !/[\r\n\u0000\\]/.test(value) && !/^(?:\/|[a-z][a-z0-9+.-]*:)/i.test(value) && !file.includes(':')
43
+ && !value.includes('\0') && !/[\r\n\\]/.test(value) && !/^(?:\/|[a-z][a-z0-9+.-]*:)/i.test(value) && !file.includes(':')
43
44
  && !file.split('/').some((part) => ['', '.', '..'].includes(part))
44
45
  && (parts.length === 1 || (anchor && parts.length === 2 && /^[A-Za-z0-9][A-Za-z0-9_.:-]*$/.test(parts[1])));
45
46
  if (!valid) bad.push(`${label} must be a repository-relative path${anchor ? ' with an optional #anchor' : ''}`);
@@ -51,7 +52,7 @@ function unknownKeys(item, allowed, label, bad) {
51
52
 
52
53
  function nonEmptyList(value, label, bad) {
53
54
  if (!Array.isArray(value) || !value.length) { bad.push(`${label} must be a non-empty list`); return false; }
54
- const normalized = value.map((value) => JSON.stringify(value, (_key, item) => isPlainObject(item) ? Object.fromEntries(Object.keys(item).sort().map((key) => [key, item[key]])) : item));
55
+ const normalized = value.map((value) => JSON.stringify(value, (_key, item) => isPlainObject(item) ? Object.fromEntries(Object.keys(item).sort(byCodeUnit).map((key) => [key, item[key]])) : item));
55
56
  if (new Set(normalized).size !== value.length) bad.push(`${label} repeats a value`);
56
57
  return true;
57
58
  }
@@ -8,7 +8,7 @@ const DEPLOY = /(?:^|\/)deploy\//;
8
8
  const SCRIPT = /(?:^|\/)scripts\//;
9
9
  const COMPOSE = /(?:^|\/)(?:docker-)?compose(?:[.-][^/]*)?\.ya?ml$/;
10
10
  const DOCKERFILE = /(?:^|\/)Dockerfile(?:\.[^/]*)?$/;
11
- const CREDENTIAL_FILE = /(?:^|\/)(?:secrets?\.env(?:\.[^/]*)?|\.env(?:\.[^/]*)?|secrets\/)|\.enc$/i;
11
+ const CREDENTIAL_FILE = /(?:(?:^|\/)(?:secrets?\.env(?:\.[^/]*)?|\.env(?:\.[^/]*)?|secrets\/))|(?:\.enc$)/i;
12
12
  const HOST_CRON = /\b(?:crontab|anacron|crond|supercronic)\b|\/etc\/cron(?:\b|[./])|\bcron\.d\b|\/var\/spool\/cron\b|@(?:reboot|yearly|annually|monthly|weekly|daily|midnight|hourly)\b|\b[\w.@-]+\.timer\b|\bOn(?:Calendar|BootSec|UnitActiveSec|ActiveSec)\s*=|(?:^|[\s;|&"'])at\s+(?:(?:-[a-z]+\s+)*)((?:-[ft]\b)|now\b|tomorrow\b|\d{1,2}:\d{2}\b|midnight\b|noon\b)/im;
13
13
  const HOST_CRON_PATH = /(?:^|\/)(?:crontab(?:$|\/)|cron\.d(?:\/|$)|[^/]+\.(?:timer|crontab)$)/;
14
14
  const CRON_NUMERIC_FIELD = /^[\d*,/-]+$/;
@@ -228,7 +228,7 @@ function injectionTokens(repoRoot, tracked) {
228
228
  const ts = loadTypescript(repoRoot, path.dirname(fileURLToPath(import.meta.url)));
229
229
  if (!ts) return tokens;
230
230
  for (const file of tracked) {
231
- if (!/\.[cm]?[jt]sx?$/.test(file) || /(?:^|\/)(?:tests?|__tests__)\/|(?:\.(?:spec|test)|-spec)\.[cm]?[jt]sx?$/.test(file)) continue;
231
+ if (!/\.[cm]?[jt]sx?$/.test(file) || /(?:(?:^|\/)(?:tests?|__tests__)\/)|(?:(?:\.(?:spec|test)|-spec)\.[cm]?[jt]sx?$)/.test(file)) continue;
232
232
  const text = readText(repoRoot, file) ?? '';
233
233
  declaredTokens(ts, ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true), tokens);
234
234
  }
@@ -37,7 +37,7 @@ function wireSchemaDigest(repoRoot, snapshots) {
37
37
  for (const snapshot of [...snapshots].sort(byCodeUnit)) {
38
38
  let source;
39
39
  try { source = fs.readFileSync(path.join(repoRoot, snapshot), 'utf8'); } catch { return null; }
40
- hash.update(`${snapshot}\0${source.replace(/\r\n/g, '\n')}\0`);
40
+ hash.update(`${snapshot}\0${source.replaceAll('\r\n', '\n')}\0`);
41
41
  }
42
42
  return hash.digest('hex');
43
43
  }
@@ -79,7 +79,7 @@ function refProof(value) {
79
79
  if (typeof value !== 'string') return 0;
80
80
  const expression = unwrap(workflowExpression(value));
81
81
  const alternatives = splitBoolean(expression, '||');
82
- if (alternatives.length > 1) return alternatives.map(refProof).reduce((a, b) => a & b);
82
+ if (alternatives.length > 1) return alternatives.map(refProof).reduce((a, b) => a & b, RELEASE_REF);
83
83
  const conjuncts = splitBoolean(expression, '&&');
84
84
  if (conjuncts.length > 1) return conjuncts.map(refProof).reduce((a, b) => a | b, 0);
85
85
  if (/^startsWith\(\s*github\.ref\s*,\s*(['"])refs\/tags\/v\1\s*\)$/.test(expression)) return RELEASE_REF;
@@ -7,7 +7,7 @@ const PLAINTEXT_LOAD = 'HFS_SCRIPT_PLAINTEXT_LOAD';
7
7
  const XTRACE = 'HFS_SCRIPT_XTRACE';
8
8
  const SERVICE_ROLE = 'HFS_SERVICE_ROLE_SCOPE';
9
9
  const SUPABASE_OWNER = 'be.integrations.supabase';
10
- const SECRET_FILE = /(?:^|\/)(?:secrets?\.env(?:\.[^/]*)?|\.env(?:\.[^/]*)?|\.nivo-lite(?:\/|$))|\.enc$/iu;
10
+ const SECRET_FILE = /(?:(?:^|\/)(?:secrets?\.env(?:\.[^/]*)?|\.env(?:\.[^/]*)?|\.nivo-lite(?:\/|$)))|(?:\.enc$)/iu;
11
11
  const EXCLUDED_TREE = /(?:^|\/)(?:node_modules|\.git|\.claude|\.secrets|secrets|tests?|__tests__|docs|coverage|dist|build)(?:\/|$)/u;
12
12
  const TEST_FILE = /(?:\.(?:spec|test)|-spec)\.[cm]?[jt]sx?$/u;
13
13
  const SOURCE = /\.(?:[cm]?[jt]sx?|sh|bash|zsh|ksh|ps1|psm1|bat|cmd|py|rb|pl)$/iu;
@@ -13,7 +13,7 @@ const SCRIPTS_DIR = /(?:^|\/)scripts\//;
13
13
  const PACKAGE_JSON = /(?:^|\/)package\.json$/;
14
14
  const SOURCE_FILE = /(?:^|\/)[^/.]+$|\.(?:sh|bash|zsh|mjs|cjs|js|ts|mts|py|ya?ml)$/;
15
15
  const TEST_FILE = /\.(?:spec|test)\.[cm]?[jt]s$/;
16
- const CREDENTIAL_FILE = /(?:^|\/)(?:secrets?\.env(?:\.[^/]*)?|\.env(?:\.[^/]*)?|secrets\/)|\.enc$/i;
16
+ const CREDENTIAL_FILE = /(?:(?:^|\/)(?:secrets?\.env(?:\.[^/]*)?|\.env(?:\.[^/]*)?|secrets\/))|(?:\.enc$)/i;
17
17
  const VERB_FILE = /^(render|provision|converge|verify|prune|rollback|release|backup|prepare|doctor|jobs|public)\.(?:mjs|js|cjs|sh)$/;
18
18
  const COMMENT_LINE = /^[ \t]*(?:#|\/\/).*$/gm;
19
19
  const LINE_CONTINUATION = /\\\r?\n\s*/g;
@@ -26,7 +26,7 @@ const DECLARATION = '.starcistacks/application-stacks.yaml';
26
26
  const MAX_BYTES = 1024 * 1024;
27
27
  const SOURCE = /^(?:be\/)?(?:src|apps)\/.*\.[cm]?[jt]s$/;
28
28
  const TEST = /(?:\.(?:spec|test)|-spec)\.[cm]?[jt]s$|\/(?:tests|__tests__)\//;
29
- const SECRET = /(?:^|\/)(?:secrets|\.claude|\.aws|\.codex)(?:\/|$)|(?:^|\/)\.env(?:\.|$)|\.(?:enc|pem|key|p12|pfx)$/i;
29
+ const SECRET = /(?:(?:^|\/)(?:secrets|\.claude|\.aws|\.codex)(?:\/|$))|(?:(?:^|\/)\.env(?:\.|$))|(?:\.(?:enc|pem|key|p12|pfx)$)/i;
30
30
  const CLI_ENTRY = /^(?:be\/)?dist\/apps\/cli\/[A-Za-z0-9_./-]+\.(?:js|mjs|cjs)$/;
31
31
  const mapping = value => plain(value) ? value : {};
32
32
  const present = value => typeof value === 'string' && Boolean(value.trim());
@@ -16,7 +16,7 @@ const mapping = value => plain(value) ? value : {};
16
16
  const present = value => typeof value === 'string' && Boolean(value.trim());
17
17
  const VERSION = /^[1-9][0-9]*$/;
18
18
  const VERSIONED_SECRET = /-v[1-9][0-9]*$/;
19
- const SENSITIVE_KEY = /password|passwd|secret|token|credential|private[_-]?key|api[_-]?key|service[_-]?role|(?:^|_)key$/i;
19
+ const SENSITIVE_KEY = /password|passwd|secret|token|credential|private[_-]?key|api[_-]?key|service[_-]?role|(?:(?:^|_)key$)/i;
20
20
  const SOURCE = /^be\/(?:apps|src)\/.*\.[cm]?[jt]sx?$/;
21
21
  const TEST_SOURCE = /(?:\.(?:spec|test)|-spec)\.[cm]?[jt]sx?$|\/tests\//;
22
22
  const REALTIME_SOURCE = /^be\/src\/features\/realtime\/[^/]+\//;
@@ -32,6 +32,7 @@ function appendShellCommand(commands, state, next) {
32
32
  }
33
33
 
34
34
  function shellLineCommands(line, commands) {
35
+ /** @type {{quote: string | null, command: string, separator: string | null}} */
35
36
  const state = { quote: null, command: '', separator: null };
36
37
  for (let i = 0; i < line.length; i++) {
37
38
  const char = line[i];
@@ -76,7 +76,7 @@ function expressions(value) {
76
76
 
77
77
  /** Source markers travel inside parsed scalar expressions, preserving flow maps, quoted keys and repeated values without matching comments. */
78
78
  export function workflowContextFindings({ path: file, text }) {
79
- const marked = text.replace(/\$\{\{/g, (match, offset) => `${match} __hfs_context_line_${text.slice(0, offset).split('\n').length}__ `);
79
+ const marked = text.replaceAll('${{', (match, offset) => `${match} __hfs_context_line_${text.slice(0, offset).split('\n').length}__ `);
80
80
  const findings = [];
81
81
  function inspect(value, keys) {
82
82
  const allowed = allowedContexts(keys);
@@ -22,7 +22,7 @@ function checksumStep(checksum, value) {
22
22
  function checksumWords(prefix, words) {
23
23
  const codes = [...prefix].map((character) => character.codePointAt(0));
24
24
  const expanded = [...codes.map((code) => code >>> 5), 0, ...codes.map((code) => code & 31)];
25
- return [...expanded, ...words].reduce(checksumStep, 1);
25
+ return [...expanded, ...words].reduce((checksum, value) => checksumStep(checksum, value), 1);
26
26
  }
27
27
 
28
28
  function convertBits(input, from, to, pad) {
@@ -40,6 +40,8 @@ export const pathsOverlap = (a, b) => sameOrUnder(a, b) || sameOrUnder(b, a);
40
40
  * `trim` blanks at the ends, `collapse` folds '//' runs to '/', `dot` drops a leading './',
41
41
  * `glob` strips a trailing glob ('star': '*'/'**' with their slash, 'double': a '/**' only),
42
42
  * `fold` lowercases. A trailing '/' always goes. Null reads as ''.
43
+ * @param {unknown} p
44
+ * @param {{trim?: boolean, collapse?: boolean, dot?: boolean, glob?: string|null, fold?: boolean}} [options]
43
45
  */
44
46
  export const normPath = (p, { trim = false, collapse = false, dot = false, glob = null, fold = false } = {}) => {
45
47
  let s = String(p ?? '');
@@ -173,7 +173,7 @@ function readmeBadgeText(text, manifestText) {
173
173
 
174
174
  /** Whole-file secret judgement exempts only declared public badge values and preserves original line numbers and every provider shape. */
175
175
  export function secretHitsInFile(file, fullText, { manifestText } = {}) {
176
- const normalized = file.replace(/\\/g, '/');
176
+ const normalized = file.replaceAll('\\', '/');
177
177
  let text = fullText;
178
178
  if (/(?:^|\/)\.starcistacks\/application-stacks\.yaml$/.test(normalized)) text = manifestBadgeText(text);
179
179
  if (/(?:^|\/)README\.md$/.test(normalized)) text = readmeBadgeText(text, manifestText);
@@ -1,9 +1,13 @@
1
1
  /** The expression under the wrappers selected by an AST check's contract. */
2
2
  export function unwrapExpression(ts, expression, { awaitExpressions = false, typeAssertions = true, nonNullExpressions = true, satisfiesExpressions = true } = {}) {
3
- while (expression && (ts.isParenthesizedExpression(expression) || ts.isAsExpression(expression)
4
- || (typeAssertions && ts.isTypeAssertionExpression(expression)) || (nonNullExpressions && ts.isNonNullExpression(expression))
5
- || (satisfiesExpressions && (ts.isSatisfiesExpression?.(expression) ?? false))
6
- || (awaitExpressions && ts.isAwaitExpression(expression)))) expression = expression.expression;
3
+ while (expression) {
4
+ const wrapper = ts.isParenthesizedExpression(expression) || ts.isAsExpression(expression)
5
+ || (typeAssertions && ts.isTypeAssertionExpression(expression)) || (nonNullExpressions && ts.isNonNullExpression(expression))
6
+ || (satisfiesExpressions && (ts.isSatisfiesExpression?.(expression) ?? false))
7
+ || (awaitExpressions && ts.isAwaitExpression(expression));
8
+ if (!wrapper) break;
9
+ expression = expression.expression;
10
+ }
7
11
  return expression;
8
12
  }
9
13