@starci/eslint-canon-fe 8.4.0 → 8.4.1
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 -0
- package/package.json +1 -1
- package/runtime/config.example.yaml +7 -6
- package/runtime/engine/config.mjs +10 -18
- package/runtime/engine/removed-vocabulary.mjs +0 -2
- package/runtime/knowledge/hfs/canon-pins.yaml +4 -4
- package/runtime/knowledge/hfs/slots.yaml +13 -24
- package/runtime/modules/kernel/removed-vocabulary.yaml +30 -0
- package/runtime/modules/models/runtimes.yaml +4 -6
- package/runtime/scripts/api/git/lib.mjs +3 -3
- package/runtime/scripts/hfs/architecture/config.mjs +1 -1
- package/runtime/scripts/hfs/architecture/index.mjs +1 -1
- package/runtime/scripts/hfs/architecture/supabase.mjs +1 -1
- package/runtime/scripts/hfs/architecture/tiers.mjs +1 -1
- package/runtime/scripts/hfs/manifest-shape.mjs +3 -3
- package/runtime/scripts/hfs/param-names.mjs +1 -1
- package/runtime/scripts/hfs/rule-catalog.mjs +3 -1
- package/runtime/scripts/hfs/slots.mjs +2 -2
- package/runtime/scripts/hfs/typescript-programs.mjs +0 -8
- package/runtime/scripts/lib/env.mjs +2 -2
- package/runtime/scripts/lib/git.mjs +1 -1
- package/runtime/scripts/lib/i18n.mjs +0 -2
- package/runtime/scripts/lib/language.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 8.4.1 - 2026-10-17
|
|
6
|
+
|
|
7
|
+
- Changed: the bundled runtime copies follow the runtime of 1.0.0-alpha.19 (owner ruling no-runtime-specs, 2026-10-17): the failure codes, the slot manifest (`exampleNa`) and the rule catalog (`specNa`, `exampleNa`) carry the removal of the runtime's own specs; package behavior is unchanged.
|
|
5
8
|
- Changed (R287 `FE_CALLBACK_SHAPE`, a18.1 NIVO request F2, owner ruling `auth-callback-pure-helpers`): the auth callback may call pure text helpers of the app's `modules/routes` or `modules/auth` (`requestOrigin`, `safeNextPath`; resolved by TypeScript symbol: synchronous, no await, no I/O global, only owner helpers and `modules/config` readers), use static route tables (`AUTH_ROUTES`), and read the exchange outcome's `success` or `ok` flag. An impure, async or non-text helper, a helper outside those owners and a credential passed to a helper are still refused.
|
|
6
9
|
- Changed (R287 `FE_CALLBACK_SHAPE`, a18.1 NIVO request F, owner ruling `auth-callback-email-link`): the auth callback may await one `verifyOtp({ token_hash, type })` of the auth owner (`auth.verifyOtp` or `verifyOtp` from `modules/auth/server`), built from exactly the query values `token_hash` and `type`, beside the one `exchangeAuthCode(code)`; the token hash never reaches a redirect.
|
|
7
10
|
- Fixed (R55 `FE_CLIENT_BOUNDARY`, a18.1 NIVO request I): the finding on a page or layout owner now says the owner stays a server component that renders the client block or overlay carrying the directive. The rule is unchanged.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starci/eslint-canon-fe",
|
|
3
|
-
"version": "8.4.
|
|
3
|
+
"version": "8.4.1",
|
|
4
4
|
"description": "The StarCi front-end lint canon: every rule its laws publish, gathered as one flat-config plugin and attached by one factory.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -185,10 +185,11 @@ sonar: {organization: null}
|
|
|
185
185
|
# minFreeRamPct free-RAM percentage below which no new heavy op starts
|
|
186
186
|
# set them only in the gitignored config.yaml copy, e.g.
|
|
187
187
|
# resources: {minFreeDiskGb: 5, minFreeDiskPct: 1, minFreeRamPct: 10}
|
|
188
|
-
# release
|
|
189
|
-
# suite: local the shipped default: `starci release cut` runs the root
|
|
190
|
-
#
|
|
191
|
-
#
|
|
192
|
-
#
|
|
193
|
-
#
|
|
188
|
+
# release - where the Linux parity of a release is judged (scripts/guards/release-suite-mode.mjs; docs/releasing.md "Where the suite runs"). One key:
|
|
189
|
+
# suite: local the shipped default: `starci release cut` runs the root rows (npm run test:packages, npm run test:e2e, npm run check) and the Linux parity container before it pushes,
|
|
190
|
+
# and the pre-push gate asks for the green root rows
|
|
191
|
+
# suite: ci the owner's recorded choice (2026-10-09): no local Linux container; the GitHub ci workflow runs the Linux parity (and SonarCloud) AFTER the push.
|
|
192
|
+
# The cut still runs the same root rows and the example apps; the record lists linux-parity as delegated, never green.
|
|
193
|
+
# A release can reach GitHub and its tag before the Linux parity has run; a red CI is fixed forward with the next pre-release (starci release ci-status).
|
|
194
|
+
# "none" is refused: the Linux parity does not vanish, it moves to CI. Set it only in the gitignored config.yaml copy: release: {suite: ci}
|
|
194
195
|
release: {suite: local}
|
|
@@ -9,7 +9,7 @@ import {validateSonar} from './sonar-config.mjs';
|
|
|
9
9
|
import {validateRelease,RELEASE_KEYS} from './release-config.mjs';
|
|
10
10
|
import {validateOrca,ORCA_KEYS} from './orca-config.mjs';
|
|
11
11
|
import {validateResources,RESOURCE_KEYS} from './resources-config.mjs';
|
|
12
|
-
import {ENV_NAME,
|
|
12
|
+
import { ENV_NAME, connectorSecret } from './secrets.mjs';
|
|
13
13
|
import {byCodeUnit} from './by-code-unit.mjs';
|
|
14
14
|
import {EFFORT_LEVELS,MODELS_KEYS,validateModelsBlock,refuseRemovedKeys,shippedTiers} from './model-config.mjs';
|
|
15
15
|
const knownNames=names=>[...names].sort(byCodeUnit).join(', ');
|
|
@@ -162,8 +162,6 @@ function validateConnectors(connectors){
|
|
|
162
162
|
if(connectors.cloudflare!==undefined)validateCloudflareConnector(connectors.cloudflare);
|
|
163
163
|
if(connectors.telegram!==undefined)validateTelegramConnector(connectors.telegram);
|
|
164
164
|
}
|
|
165
|
-
/** Load connector credentials from the explicitly verified runtime main; never print the returned environment. */
|
|
166
|
-
export function connectorEnv(config,env=process.env,verifiedRuntimeRoot=undefined){return secretEnv(verifiedRuntimeRoot,env);}
|
|
167
165
|
/**
|
|
168
166
|
* The normalized `connectors` block: defaults filled in, the secrets' PRESENCE (booleans, never values),
|
|
169
167
|
* whether each connector is ready to run, and the owner-facing warnings the security posture earns.
|
|
@@ -243,8 +241,8 @@ export function uatSettings(config=loadConfig()){
|
|
|
243
241
|
const GRANT=/^([a-z0-9][a-z0-9.-]*)=(\d+)@([a-z]+(?:\+[a-z]+)*)$/;
|
|
244
242
|
/** One grant string `<pool>=<slots>@<role>+<role>` as {pool, slots, roles}, or null when it is not that shape. */
|
|
245
243
|
/** The closed set of top-level config.yaml blocks: the validator accepts these and refuses every other key. */
|
|
246
|
-
|
|
247
|
-
|
|
244
|
+
const CONFIG_BLOCKS=Object.freeze(['language','model','effort','models','allocation','kernel','budgets','supervisor','parallel','delegation','connectors','asks','uat','specs','reconciler','debugLoop','orca','roots','resources','sonar','release','launchTrust','retention']);
|
|
245
|
+
function parseAllocationGrant(text){
|
|
248
246
|
const m=typeof text==='string'?GRANT.exec(text.trim()):null;
|
|
249
247
|
return m?{pool:m[1],slots:Number(m[2]),roles:m[3].split('+')}:null;
|
|
250
248
|
}
|
|
@@ -327,7 +325,7 @@ const validateEarlyConfigBlocks=(config)=>{
|
|
|
327
325
|
if(config?.orca!==undefined){validateOrca(config.orca);} if(config?.roots!==undefined){validateRoots(config.roots);} if(config?.resources!==undefined){validateResources(config.resources);} if(config?.sonar!==undefined){validateSonar(config.sonar);} if(config?.release!==undefined){validateRelease(config.release);}
|
|
328
326
|
};
|
|
329
327
|
const validateConfigBlocks=(config,knownProviders,runtimes,profile)=>{
|
|
330
|
-
// specs (owner 2026-09-28): {
|
|
328
|
+
// specs (owner 2026-09-28): {unit?, e2e?} booleans - each family a boolean; absent = its default (SPEC_DEFAULTS: unit on, e2e off; specsSettings).
|
|
331
329
|
if(config?.specs!==undefined&&config.specs!==null&&(!plain(config.specs)||Object.keys(config.specs).some(key=>!CONFIG_KEY_TREE.specs.includes(key)||typeof config.specs[key]!=='boolean')))throw new Error(`Invalid config.yaml: specs must be {${SPEC_FAMILIES.map(k=>k+'?: boolean').join(', ')}}, or null.`);
|
|
332
330
|
// reconciler (scripts/reconciler/state.mjs reconcilerConfig): {enabled?: boolean, profile?: operational|observe, controllers?: {<name>: {mode: off|shadow|active}}}, or null.
|
|
333
331
|
if(config?.reconciler!==undefined&&config.reconciler!==null){validateReconcilerBlock(config.reconciler);} if(config?.allocation!==undefined){validateAllocationBlock(config.allocation,runtimes);}
|
|
@@ -356,8 +354,8 @@ export function configuredAllocationPolicy(config=loadConfig()){
|
|
|
356
354
|
:null;
|
|
357
355
|
return {grants,source:allocation?'allocation':'default'};
|
|
358
356
|
}
|
|
359
|
-
/** The variable that confines the owner's `config.yaml` to a directory tree: set, an owner file is read only from under that directory (
|
|
360
|
-
|
|
357
|
+
/** The variable that confines the owner's `config.yaml` to a directory tree: set, an owner file is read only from under that directory (e2e/setup/isolated-temp.mjs points it at a spec's own temp root). */
|
|
358
|
+
const OWNER_CONFIG_WITHIN_ENV='STARCI_OWNER_CONFIG_WITHIN';
|
|
361
359
|
const foldPath=p=>process.platform==='win32'?path.resolve(p).toLowerCase():path.resolve(p);
|
|
362
360
|
/**
|
|
363
361
|
* Whether the owner's `config.yaml` under `root` is visible to this process. Unconfined (the variable unset) every owner file is visible; confined, only the files under the named
|
|
@@ -398,31 +396,25 @@ export function inspectOwnerConfig(root=configRoot){
|
|
|
398
396
|
}
|
|
399
397
|
/**
|
|
400
398
|
* The owner's spec switches (config.yaml root `specs`, owner 2026-09-28; defaults set 2026-09-29, none needs a config block):
|
|
401
|
-
* harness - .claude's own specs. Default false = touching-only: harness work writes and runs the specs of new or changed code
|
|
402
|
-
* and the land gate runs only the specs touching the landed files (`land.mjs --specs touching`, its default);
|
|
403
|
-
* the whole suite never runs in a land - `--specs all` is refused unless this is true. The full suite runs in
|
|
404
|
-
* the owner-approved /starci release flow (scripts/supervisor/release-cut.mjs), which needs no key. true = `--specs all` may run;
|
|
405
399
|
* unit - product unit specs in workflows. Default on: a code-writing op writes/updates the unit specs of the source it
|
|
406
400
|
* adds or changes and runs only those; the full unit suite is unit.verify's or the approved /starci release flow's. false = the ops'
|
|
407
401
|
* policy.specsToggle deferral path (scripts/route/spec-deferral.mjs);
|
|
408
402
|
* e2e - product e2e in workflows. Default OFF: e2e runs only when the goal or the owner explicitly asks (set true,
|
|
409
403
|
* or `starci kernel run-deferred-tests`); e2e.verify then runs the FULL e2e suite.
|
|
410
|
-
* An absent, null or unreadable owner file reads as the defaults:
|
|
404
|
+
* An absent, null or unreadable owner file reads as the defaults: unit on, e2e off. The runtime itself has no specs (owner ruling no-runtime-specs), so there is no switch for them.
|
|
411
405
|
*/
|
|
412
|
-
const SPEC_FAMILIES=Object.freeze(['
|
|
406
|
+
const SPEC_FAMILIES=Object.freeze(['unit','e2e']);
|
|
413
407
|
const SEAT_KEYS=Object.freeze(['agent','model','effort']);
|
|
414
408
|
/** The keys each config block accepts directly under it (the validators read these lists; docs/config-format.md and the example are checked against them). Blocks whose keys are open or deeper than this are not listed. */
|
|
415
|
-
|
|
409
|
+
const CONFIG_KEY_TREE=Object.freeze({
|
|
416
410
|
models:MODELS_KEYS,allocation:Object.freeze(['grants']),kernel:SEAT_KEYS,budgets:Object.freeze(['maxOps']),parallel:Object.freeze(['gear']),
|
|
417
411
|
supervisor:Object.freeze(['mode','pollIntervalMs','repos','stallMinutes','kernel','workers','landGate','frozenMinutes']),
|
|
418
412
|
delegation:Object.freeze(['asks','until','excludes','note']),asks:Object.freeze(['autoAcceptRecommended','excludes']),uat:Object.freeze(['maxConcurrent']),
|
|
419
413
|
specs:SPEC_FAMILIES,reconciler:Object.freeze(['enabled','profile','controllers']),debugLoop:Object.freeze(['interval','worktreeLimit']),
|
|
420
414
|
orca:ORCA_KEYS,roots:ROOT_KEYS,resources:RESOURCE_KEYS,release:RELEASE_KEYS,
|
|
421
415
|
});
|
|
422
|
-
export const SPEC_DEFAULTS=Object.freeze({
|
|
416
|
+
export const SPEC_DEFAULTS=Object.freeze({unit:true,e2e:false});
|
|
423
417
|
export function specsSettings(config){const specs=plain(config?.specs)?config.specs:{};return Object.fromEntries(SPEC_FAMILIES.map(key=>[key,typeof specs[key]==='boolean'?specs[key]:SPEC_DEFAULTS[key]]));}
|
|
424
|
-
/** specs.harness of the owner file under `root` (tolerant read: inspectOwnerConfig): true only when the owner opted in to `--specs all`. */
|
|
425
|
-
export function harnessSpecsEnabled(root=configRoot){return specsSettings(inspectOwnerConfig(root).config).harness;}
|
|
426
418
|
/**
|
|
427
419
|
* config.yaml `debugLoop` (skills/starci, scripts/debug/digest): {interval?, worktreeLimit?}, each optional.
|
|
428
420
|
* interval <n>s | <n>m | <n>h — the cadence of the chat /loop that runs `starci debug digest`.
|
|
@@ -12,8 +12,6 @@ function loaded(){
|
|
|
12
12
|
return cache.doc;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
/** The kinds a removed spelling has. */
|
|
16
|
-
export const removedKinds=()=>loaded().kinds;
|
|
17
15
|
|
|
18
16
|
/** modules/kernel/removed-vocabulary.yaml: the one list of spellings the runtime refuses, each with its replacement and the release that removed it. */
|
|
19
17
|
export const removedVocabulary=()=>loaded().removed;
|
|
@@ -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.
|
|
32
|
+
version: 3.4.1
|
|
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.
|
|
39
|
+
version: 8.4.1
|
|
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.4.
|
|
79
|
+
version: 1.4.1
|
|
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.4.
|
|
86
|
+
version: 4.4.1
|
|
87
87
|
group: starci
|
|
88
88
|
install: registry
|
|
89
89
|
side: both
|
|
@@ -535,7 +535,6 @@ slots:
|
|
|
535
535
|
profiles: [app]
|
|
536
536
|
exemplifiedBy:
|
|
537
537
|
- packages/hfs/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml
|
|
538
|
-
- {path: tests/fixtures/hfs-callback-full/.github/workflows/db-deploy.yml, edition: full}
|
|
539
538
|
editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): lite manages its Supabase database deploy workflow; full owns an optional provider-specific workflow."
|
|
540
539
|
# The database deploy workflow: workflow_dispatch only (like app.ci-e2e); starci app sync renders it under the managed ci group.
|
|
541
540
|
path: ".github/workflows/db-deploy.yml"
|
|
@@ -546,6 +545,7 @@ slots:
|
|
|
546
545
|
liteManagedBy: ci-workflows-lite
|
|
547
546
|
rules: [HFS_MANAGED_FILE_DRIFT]
|
|
548
547
|
since: 2.1.0
|
|
548
|
+
exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
|
|
549
549
|
- id: app.ci-stack-deploy
|
|
550
550
|
since: 3.3.0
|
|
551
551
|
profiles: [app]
|
|
@@ -657,8 +657,6 @@ slots:
|
|
|
657
657
|
- id: app.scripts.delivery
|
|
658
658
|
since: 3.1.0
|
|
659
659
|
profiles: [app]
|
|
660
|
-
exemplifiedBy:
|
|
661
|
-
- {path: tests/fixtures/hfs-delivery-lite/scripts/build-frontend.mjs, edition: lite}
|
|
662
660
|
editionWhy: "Lite splits product delivery tooling from managed codegen; full owns the same files through app.scripts (ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4))."
|
|
663
661
|
editions: [lite]
|
|
664
662
|
# Product-owned environment, secret and build tooling, kept apart from lite's managed codegen script.
|
|
@@ -669,6 +667,7 @@ slots:
|
|
|
669
667
|
tier: none
|
|
670
668
|
tests: none
|
|
671
669
|
rules: [HFS_PLAINTEXT_SECRET, HFS_SCRIPT_ONE_OFF, BE_SPEC_PLACEMENT, HFS_REPO_LOCAL_CHECK]
|
|
670
|
+
exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
|
|
672
671
|
- id: app.sides
|
|
673
672
|
since: 3.1.0
|
|
674
673
|
profiles: [app]
|
|
@@ -791,7 +790,6 @@ slots:
|
|
|
791
790
|
profiles: [app]
|
|
792
791
|
exemplifiedBy:
|
|
793
792
|
- packages/hfs/templates/app/skeleton-lite/supabase/
|
|
794
|
-
- {path: tests/fixtures/hfs-callback-full/supabase/, edition: full}
|
|
795
793
|
editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): Supabase is required in lite; full enables it only through a declared connection."
|
|
796
794
|
# The one Supabase project directory, at the app root beside be/, fe/ and .starcistacks (never under a side).
|
|
797
795
|
path: "supabase/"
|
|
@@ -802,11 +800,11 @@ slots:
|
|
|
802
800
|
tier: none
|
|
803
801
|
tests: none
|
|
804
802
|
since: 2.1.0
|
|
803
|
+
exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
|
|
805
804
|
- id: app.supabase.config
|
|
806
805
|
profiles: [app]
|
|
807
806
|
exemplifiedBy:
|
|
808
807
|
- packages/hfs/templates/app/skeleton-lite/supabase/config.toml
|
|
809
|
-
- {path: tests/fixtures/hfs-callback-full/supabase/config.toml, edition: full}
|
|
810
808
|
editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite requires Supabase configuration; full requires it only when Supabase is declared."
|
|
811
809
|
# The project configuration: auth keys are env(NAME) references, never literals (L08 judges its policy keys).
|
|
812
810
|
path: "supabase/config.toml"
|
|
@@ -817,11 +815,11 @@ slots:
|
|
|
817
815
|
tier: none
|
|
818
816
|
tests: none
|
|
819
817
|
since: 2.1.0
|
|
818
|
+
exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
|
|
820
819
|
- id: app.supabase.migrations
|
|
821
820
|
profiles: [app]
|
|
822
821
|
exemplifiedBy:
|
|
823
822
|
- packages/hfs/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql
|
|
824
|
-
- {path: tests/fixtures/hfs-callback-full/supabase/migrations/20261014100000_baseline.sql, edition: full}
|
|
825
823
|
editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite uses Supabase schema authority and requires a migration; full opts into that provider."
|
|
826
824
|
# The schema authority of an app on Supabase: forward-only SQL migrations named <timestamp14>_<kebab-name>.sql,
|
|
827
825
|
# one file per change, immutable once on origin/main (L02-L08 judge them).
|
|
@@ -834,6 +832,7 @@ slots:
|
|
|
834
832
|
tier: none
|
|
835
833
|
tests: none
|
|
836
834
|
since: 2.1.0
|
|
835
|
+
exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
|
|
837
836
|
- id: app.supabase.migrations.archive
|
|
838
837
|
profiles: [app]
|
|
839
838
|
# The migrations an existing app retired at its cutover (owner ruling 2026-10-13, migration baseline policy): the live schema is
|
|
@@ -858,8 +857,6 @@ slots:
|
|
|
858
857
|
- id: app.supabase.types
|
|
859
858
|
profiles: [app]
|
|
860
859
|
templateNa: "Database row types are generated from the app's Supabase schema by contract:emit; a hand-authored scaffold template would invent the schema contract."
|
|
861
|
-
exemplifiedBy:
|
|
862
|
-
- {path: tests/fixtures/hfs-callback-full/supabase/types/database.types.ts, edition: full}
|
|
863
860
|
editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite requires generated Supabase row types; full enables them with the provider."
|
|
864
861
|
# The generated Database types (database.types.ts) contract:emit writes from the migrations; both sides read it
|
|
865
862
|
# (sides.<side>.reads supabase/types/); drift against regeneration is L09.
|
|
@@ -871,6 +868,7 @@ slots:
|
|
|
871
868
|
tier: none
|
|
872
869
|
tests: none
|
|
873
870
|
since: 2.1.0
|
|
871
|
+
exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
|
|
874
872
|
- id: app.supabase.templates
|
|
875
873
|
profiles: [app]
|
|
876
874
|
path: "supabase/templates/*.html"
|
|
@@ -1006,10 +1004,8 @@ slots:
|
|
|
1006
1004
|
tier: none
|
|
1007
1005
|
tests: none
|
|
1008
1006
|
coverage: none
|
|
1009
|
-
exemplifiedBy:
|
|
1010
|
-
- {path: tests/fixtures/hfs-i18n-ledger/fe/i18n-dynamic-keys.yaml, editions: [lite, full]}
|
|
1011
|
-
- {path: tests/fixtures/hfs-i18n-ledger/be/i18n-dynamic-keys.yaml, editions: [lite, full]}
|
|
1012
1007
|
rules: [HFS_UNUSED_SYMBOL]
|
|
1008
|
+
exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
|
|
1013
1009
|
- id: repo.packages
|
|
1014
1010
|
since: 3.1.0
|
|
1015
1011
|
profiles: [be, fe]
|
|
@@ -1401,8 +1397,6 @@ slots:
|
|
|
1401
1397
|
since: 1.0.0
|
|
1402
1398
|
- id: be.feature.realtime
|
|
1403
1399
|
profiles: [be]
|
|
1404
|
-
exemplifiedBy:
|
|
1405
|
-
- {path: tests/fixtures/hfs-lite-slots/be/src/features/realtime/order-status/, edition: lite}
|
|
1406
1400
|
editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite runs socket.io in the api process, one instance without Redis; full may scale its gateway."
|
|
1407
1401
|
path: "src/features/realtime/<channel>/"
|
|
1408
1402
|
presence: opt-in # a push channel: reads and pushes only (a socket gateway or a GraphQL subscription), no domain write
|
|
@@ -1422,6 +1416,7 @@ slots:
|
|
|
1422
1416
|
coverage: none
|
|
1423
1417
|
rules: [BE_KIND_ISOLATION, BE_FEATURE_IMPORTS_FEATURE, BE_FEATURE_NOT_COMPOSED]
|
|
1424
1418
|
since: 1.0.0
|
|
1419
|
+
exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
|
|
1425
1420
|
- id: be.feature.realtime.graphql
|
|
1426
1421
|
profiles: [be]
|
|
1427
1422
|
editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite uses only the in-process socket.io gateway; the GraphQL subscription door remains full-only."
|
|
@@ -1607,8 +1602,6 @@ slots:
|
|
|
1607
1602
|
- id: be.platform.event-bus
|
|
1608
1603
|
since: 3.1.0
|
|
1609
1604
|
profiles: [be]
|
|
1610
|
-
exemplifiedBy:
|
|
1611
|
-
- {path: tests/fixtures/hfs-lite-slots/be/src/modules/platform/event-bus/, edition: lite}
|
|
1612
1605
|
editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite implements the event ports with outbox and pgmq; full uses its Kafka adapter."
|
|
1613
1606
|
path: "src/modules/platform/event-bus/"
|
|
1614
1607
|
presence: opt-in
|
|
@@ -1627,6 +1620,7 @@ slots:
|
|
|
1627
1620
|
coverage: required
|
|
1628
1621
|
budget: {indexExports: 60}
|
|
1629
1622
|
rules: [BE_TIER_DIRECTION, ARCH_OWNER_CYCLE, BE_MODULE_SHAPE, BE_INFRA_OWNER]
|
|
1623
|
+
exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
|
|
1630
1624
|
- id: be.platform.queue
|
|
1631
1625
|
since: 3.1.0
|
|
1632
1626
|
profiles: [be]
|
|
@@ -1750,8 +1744,6 @@ slots:
|
|
|
1750
1744
|
- id: be.projections
|
|
1751
1745
|
since: 3.1.0
|
|
1752
1746
|
profiles: [be]
|
|
1753
|
-
exemplifiedBy:
|
|
1754
|
-
- {path: tests/fixtures/hfs-lite-slots/be/src/modules/projections/order-summary/, edition: lite}
|
|
1755
1747
|
editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite projection tables live in Supabase migrations; full keeps the TypeORM projection entity beside its projection."
|
|
1756
1748
|
path: "src/modules/projections/<name>/"
|
|
1757
1749
|
presence: opt-in
|
|
@@ -1767,6 +1759,7 @@ slots:
|
|
|
1767
1759
|
tests: unit-beside # a measured slot: <name>.projection.spec.ts beside its projection is legal (the file owes 100 per file; a spec of its own is optional)
|
|
1768
1760
|
coverage: required
|
|
1769
1761
|
rules: [BE_PROJECTION_WRITE_OWNER, BE_PROJECTION_SHAPE, BE_TIER_DIRECTION, BE_CONTEXT_COUPLING]
|
|
1762
|
+
exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
|
|
1770
1763
|
- id: be.integrations
|
|
1771
1764
|
since: 3.1.0
|
|
1772
1765
|
profiles: [be]
|
|
@@ -1796,7 +1789,6 @@ slots:
|
|
|
1796
1789
|
profiles: [be]
|
|
1797
1790
|
exemplifiedBy:
|
|
1798
1791
|
- packages/hfs/templates/be/skeleton-lite/src/modules/integrations/supabase/
|
|
1799
|
-
- {path: tests/fixtures/hfs-callback-full/be/src/modules/integrations/supabase/, edition: full}
|
|
1800
1792
|
editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite requires the Supabase Auth and Storage integration; full opts in through a connection."
|
|
1801
1793
|
# The only be import of @supabase/* (L10): the Auth admin and Storage admin client, plus the JWKS-based verification of
|
|
1802
1794
|
# the Supabase access token the guard chain reads (L16). The connection itself is ordinary Postgres (the shared
|
|
@@ -1814,6 +1806,7 @@ slots:
|
|
|
1814
1806
|
rules: [BE_TIER_DIRECTION, BE_ERROR_HOME, BE_SECRET_DEFAULT]
|
|
1815
1807
|
coverage: required
|
|
1816
1808
|
since: 2.1.0
|
|
1809
|
+
exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
|
|
1817
1810
|
|
|
1818
1811
|
# ----- BE: tests (owner test layout 2026-09-30). Unit specs are <name>.service.spec.ts beside their service and nothing else; everything else
|
|
1819
1812
|
# lives under src/tests/, one folder per kind, and the folder and the file suffix always agree (a file whose suffix does
|
|
@@ -1821,8 +1814,6 @@ slots:
|
|
|
1821
1814
|
- id: be.tests.world
|
|
1822
1815
|
since: 3.1.0
|
|
1823
1816
|
profiles: [be]
|
|
1824
|
-
exemplifiedBy:
|
|
1825
|
-
- {path: tests/fixtures/hfs-lite-slots/be/src/tests/world/, edition: lite}
|
|
1826
1817
|
editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite integration proofs use the local Supabase stack and network fakes; full also boots apps and self-hosted services."
|
|
1827
1818
|
path: "src/tests/world/"
|
|
1828
1819
|
presence: optional
|
|
@@ -1848,6 +1839,7 @@ slots:
|
|
|
1848
1839
|
overridden in the DI container. It is the only test location that may import typeorm's DataSource or testcontainers,
|
|
1849
1840
|
call migrate/runMigrations/synchronize, or write process.env.
|
|
1850
1841
|
rules: [BE_TEST_TOPOLOGY, BE_TEST_CONSTRAINTS_ON]
|
|
1842
|
+
exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
|
|
1851
1843
|
|
|
1852
1844
|
- id: be.tests.world.kit
|
|
1853
1845
|
since: 3.1.0
|
|
@@ -2015,8 +2007,6 @@ slots:
|
|
|
2015
2007
|
rules: [FE_NEXT_CONVENTIONS]
|
|
2016
2008
|
- id: fe.route.callback
|
|
2017
2009
|
profiles: [fe]
|
|
2018
|
-
exemplifiedBy:
|
|
2019
|
-
- {path: tests/fixtures/hfs-callback-full/fe/apps/web/src/app/auth/callback/route.ts, edition: full}
|
|
2020
2010
|
# The auth callback of a Supabase session (the PKCE code exchange), kept only when the auth flow truly needs a server exchange
|
|
2021
2011
|
# (prefer the client-side PKCE exchange in fe.modules.auth): the one non-health route handler of an app with a Supabase
|
|
2022
2012
|
# connection; an app without one has no route handler in fe/ (L15).
|
|
@@ -2246,8 +2236,7 @@ slots:
|
|
|
2246
2236
|
tests: none
|
|
2247
2237
|
why: the canonical GraphQL client output of codegen is local and never committed; this specific slot overrides the tracked API parent
|
|
2248
2238
|
rules: [FE_WIRE_GENERATED]
|
|
2249
|
-
|
|
2250
|
-
- {path: tests/fixtures/hfs-generated-wire/fe/apps/web/src/modules/api/graphql/__generated__/graphql.ts, editions: [lite, full]}
|
|
2239
|
+
exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
|
|
2251
2240
|
- id: fe.transport.client
|
|
2252
2241
|
since: 3.1.0
|
|
2253
2242
|
profiles: [fe]
|
|
@@ -309,3 +309,33 @@ removed:
|
|
|
309
309
|
name: anonymousActions
|
|
310
310
|
use: 'the front end holds no Server Action (R243 FE_SERVER_ACTION_FORBIDDEN), so no slot lists anonymous ones'
|
|
311
311
|
since: 1.0.0-alpha.13
|
|
312
|
+
- id: verb-test-affected
|
|
313
|
+
kind: verb
|
|
314
|
+
name: starci test affected
|
|
315
|
+
use: 'the runtime has no specs of its own (owner ruling no-runtime-specs): `starci runtime verify` runs the check, `starci release clean-test --base <ref>` and `starci release scaffold-e2e`'
|
|
316
|
+
since: 1.0.0-alpha.19
|
|
317
|
+
match: '\bstarci test affected\b'
|
|
318
|
+
- id: verb-test-run
|
|
319
|
+
kind: verb
|
|
320
|
+
name: starci test run
|
|
321
|
+
use: 'the L1 to L4 spec ladder is gone with the runtime specs; `starci runtime verify` proves a change, `starci release cut` runs the release rows'
|
|
322
|
+
since: 1.0.0-alpha.19
|
|
323
|
+
match: '\bstarci test run\b'
|
|
324
|
+
- id: verb-gate-runtime-coverage
|
|
325
|
+
kind: verb
|
|
326
|
+
name: starci gate runtime-coverage
|
|
327
|
+
use: 'the runtime measures no coverage of its own; the example apps keep their Codecov flags'
|
|
328
|
+
since: 1.0.0-alpha.19
|
|
329
|
+
match: '\bgate runtime-coverage\b'
|
|
330
|
+
- id: verb-release-env-test
|
|
331
|
+
kind: verb
|
|
332
|
+
name: starci release env-test
|
|
333
|
+
use: 'the release rows are the package clean-test (`starci release clean-test`), the scaffold e2e (`starci release scaffold-e2e`) and the runtime check'
|
|
334
|
+
since: 1.0.0-alpha.19
|
|
335
|
+
match: '\brelease env-test\b'
|
|
336
|
+
- id: config-specs-harness
|
|
337
|
+
kind: config-key
|
|
338
|
+
name: specs.harness
|
|
339
|
+
use: 'the runtime has no specs of its own (owner ruling no-runtime-specs); specs.unit and specs.e2e switch the product workflow tests'
|
|
340
|
+
since: 1.0.0-alpha.19
|
|
341
|
+
match: '\bspecs\.harness\b'
|
|
@@ -199,9 +199,8 @@ allocation:
|
|
|
199
199
|
# revision still differs from the live one that long past minIntervalMs + checkMs + handoverMs is a drift row (scripts/reconciler/drift.mjs).
|
|
200
200
|
selfReload: {minIntervalMs: 300000, handoverMs: 30000, checkMs: 60000, driftGraceMs: 120000}
|
|
201
201
|
# deploy (scripts/reconciler/runtime-deploy.mjs): a deploy waits up to waitMs, polling every pollMs, for the settles, Critic runs and prepared
|
|
202
|
-
# decisions in flight to finish; after the engine restart it waits up to verifyMs for the new leader's fresh heartbeat on the new revision.
|
|
203
|
-
|
|
204
|
-
deploy: {waitMs: 1800000, pollMs: 5000, verifyMs: 120000, affectedMarginMs: 300000}
|
|
202
|
+
# decisions in flight to finish; after the engine restart it waits up to verifyMs for the new leader's fresh heartbeat on the new revision.
|
|
203
|
+
deploy: {waitMs: 1800000, pollMs: 5000, verifyMs: 120000}
|
|
205
204
|
# dispatchRefusal (scripts/kernel/dispatch-refusal-memo.mjs): a ready job whose dispatch is refused for the same cause is tried again after baseMs, then twice as long each time (the one retry budget), up to capMs; a change of
|
|
206
205
|
# the cause's fingerprint (runtime revision, a ruling, a resource the refusal names) tries it at once.
|
|
207
206
|
dispatchRefusal: {baseMs: 60000, capMs: 900000}
|
|
@@ -210,9 +209,8 @@ allocation:
|
|
|
210
209
|
footprint: {everyMs: 600000, lockStaleMs: 60000}
|
|
211
210
|
# jobGuard (scripts/guards/hook-install.mjs): a guard file older than ttlMs is pruned.
|
|
212
211
|
jobGuard: {ttlMs: 604800000}
|
|
213
|
-
# landGate (scripts/supervisor/land.mjs): a land waits waitMs for its turn at the gate
|
|
214
|
-
|
|
215
|
-
landGate: {waitMs: 1800000, specsBaseMs: 600000, perSpecMs: 60000, specConcurrency: 6, cliVerbs: 12}
|
|
212
|
+
# landGate (scripts/supervisor/land.mjs): a land waits waitMs for its turn at the gate.
|
|
213
|
+
landGate: {waitMs: 1800000}
|
|
216
214
|
# workerClose (scripts/machine/worker-close.mjs; owner rule: a finished worker is closed COMPLETELY): after worker-release and the terminal
|
|
217
215
|
# close the runtime waits verifyMs for every process of that terminal's shell tree to end (polled every pollMs); a survivor proven to belong
|
|
218
216
|
# to the tree is stopped and given stopVerifyMs to end before the finding is raised.
|
|
@@ -20,7 +20,7 @@ import { withTempEnv } from '../../../engine/temp-root.mjs';
|
|
|
20
20
|
* is a request for raw bytes, not an encoding: spawnSync encodes a string `input` with it and throws
|
|
21
21
|
* ERR_UNKNOWN_ENCODING, so it reaches spawnSync as encoding:null (Buffer output either way).
|
|
22
22
|
*/
|
|
23
|
-
|
|
23
|
+
const gitSpawn = (file, args, options = {}) => {
|
|
24
24
|
assertMutationFence({ kind: 'git-effect', args });
|
|
25
25
|
const spawn = { encoding: 'utf8', windowsHide: true, ...options };
|
|
26
26
|
if (spawn.encoding === 'buffer') spawn.encoding = null;
|
|
@@ -76,5 +76,5 @@ export const gitExecutable = (options = null) => {
|
|
|
76
76
|
return found;
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
-
/** The output ceiling of the whole-diff and `cat-file --batch` calls
|
|
80
|
-
|
|
79
|
+
/** The output ceiling of the whole-diff and `cat-file --batch` calls batch (a release diff is tens of megabytes). */
|
|
80
|
+
const BATCH_MAX_BUFFER = 1024 * 1024 * 1024;
|
|
@@ -162,7 +162,7 @@ const machineIds = () => [
|
|
|
162
162
|
* backend composition and data machine and frontend repository machine. A spec about another rule judges its own
|
|
163
163
|
* report without these (they have their own specs); every one of them is in ARCHITECTURE_RULE_IDS.
|
|
164
164
|
*/
|
|
165
|
-
|
|
165
|
+
const HFS_MACHINE_RULE_IDS = Object.freeze((() => {
|
|
166
166
|
const ordinary = new Set([...COMMON_RULE_IDS, ...BACKEND_RULE_IDS, ...FRONTEND_RULE_IDS, ...OWNER_RULE_IDS, ...GRAMMAR_RULE_IDS, ...REGISTRATION_RULE_IDS,
|
|
167
167
|
...SWR_DATA_RULE_IDS, SOURCE_LAYOUT_RULE_ID, SOURCE_NAME_RULE_ID, PUBLIC_CONTRACT_RULE_ID, READONLY_BOUNDARY_RULE_ID]);
|
|
168
168
|
return [...new Set(machineIds().filter((id) => !ordinary.has(id)))].sort(byCodeUnit);
|
|
@@ -67,7 +67,7 @@ function finishFrame(state, frame) {
|
|
|
67
67
|
if (state.low.get(frame.node) === state.index.get(frame.node)) closeComponent(state, frame);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
function stronglyConnected(adjacency) {
|
|
71
71
|
const state = { counter: 0, index: new Map(), low: new Map(), onStack: new Set(), stack: [], result: [], work: [] };
|
|
72
72
|
for (const start of adjacency.keys()) {
|
|
73
73
|
if (state.index.has(start)) continue;
|
|
@@ -56,8 +56,6 @@ export const unitRolesOf = (manifest) => manifest.ruleParams.be.unitRoles;
|
|
|
56
56
|
/** The roles that carry logic (ruleParams.be.logicRoles): measured at 100 per file inside a coverage-required slot. */
|
|
57
57
|
export const logicRolesOf = (manifest) => manifest.ruleParams.be.logicRoles;
|
|
58
58
|
|
|
59
|
-
/** The roles a feature-tier file is judged thin by (ruleParams.be.thinRoles, R203 BE_FEATURE_THIN). */
|
|
60
|
-
export const thinRolesOf = (manifest) => manifest.ruleParams.be.thinRoles;
|
|
61
59
|
|
|
62
60
|
/** Shape problems of ruleParams.be.unitRoles: unique roles of the suffix vocabulary, each spec `<role>.spec`. */
|
|
63
61
|
export function unitRolesProblems(be) {
|
|
@@ -86,7 +84,7 @@ export const MANIFEST_KINDS = [APP_KIND, RUNTIME_KIND];
|
|
|
86
84
|
export const manifestKind = (m) => (isPlainObject(m) && m.kind !== undefined ? m.kind : APP_KIND);
|
|
87
85
|
const strList = (v) => stringList(v);
|
|
88
86
|
|
|
89
|
-
const APP_SLOT_KEYS = ['id', 'profiles', 'path', 'presence', 'tracked', 'tier', 'tests', 'owner', 'appKind', 'minInstances', 'requiredWhen', 'requiredInstances', 'requires', 'pattern', 'trigger', 'allows', 'forbids', 'layers', 'kinds', 'roles', 'composedBy', 'budget', 'managedBy', 'liteManagedBy', 'rules', 'goesTo', 'why', 'since', 'perConnection', 'parent', 'editions', 'editionWhy', 'litePresence', 'lite', 'provider', 'coverage', 'entries', 'outcomeHome', 'templateNa', 'exemplifiedBy'];
|
|
87
|
+
const APP_SLOT_KEYS = ['id', 'profiles', 'path', 'presence', 'tracked', 'tier', 'tests', 'owner', 'appKind', 'minInstances', 'requiredWhen', 'requiredInstances', 'requires', 'pattern', 'trigger', 'allows', 'forbids', 'layers', 'kinds', 'roles', 'composedBy', 'budget', 'managedBy', 'liteManagedBy', 'rules', 'goesTo', 'why', 'since', 'perConnection', 'parent', 'editions', 'editionWhy', 'litePresence', 'lite', 'provider', 'coverage', 'entries', 'outcomeHome', 'templateNa', 'exampleNa', 'exemplifiedBy'];
|
|
90
88
|
/** The fields a slot's `lite` overlay may hold: the same keys it would carry in the slot body, resolved under edition lite. */
|
|
91
89
|
const LITE_OVERLAY_KEYS = ['path', 'requires', 'allows', 'forbids', 'minInstances', 'requiredInstances', 'why'];
|
|
92
90
|
/** A runtime slot has no app kind, side composition, layer or managed template; it may name the generator of a generated copy. */
|
|
@@ -220,6 +218,8 @@ function slotCoverageEvidenceProblems(slot, at) {
|
|
|
220
218
|
const bad = [];
|
|
221
219
|
if (slot.templateNa !== undefined && !(typeof slot.templateNa === 'string' && slot.templateNa.trim()))
|
|
222
220
|
bad.push(`${at}: templateNa must be non-empty text`);
|
|
221
|
+
if (slot.exampleNa !== undefined && !(typeof slot.exampleNa === 'string' && slot.exampleNa.trim()))
|
|
222
|
+
bad.push(`${at}: exampleNa must be non-empty text`);
|
|
223
223
|
if (slot.exemplifiedBy === undefined) return bad;
|
|
224
224
|
const refs = slot.exemplifiedBy;
|
|
225
225
|
const valid = Array.isArray(refs) && refs.length > 0 && refs.every(slotEvidenceRefValid)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// param-names.mjs - the one shape of `ruleParams.be.paramNames` of knowledge/hfs/slots.yaml (R85 injected-param-name): a non-empty list of unique
|
|
2
2
|
// entries `{type | typeSuffix, names, nameSuffix?}`. The loader of scripts/hfs/slots.mjs calls it; the schema (modules/schemas/hfs-slots.schema.yaml)
|
|
3
|
-
// states the same shape
|
|
3
|
+
// states the same shape.
|
|
4
4
|
import { isPlainObject } from '../../engine/plain-object.mjs';
|
|
5
5
|
|
|
6
6
|
const UPPER = /^[A-Z][A-Za-z0-9]*$/u;
|
|
@@ -16,7 +16,7 @@ const RULE_KINDS = Object.freeze(['codemod', 'lint', 'check', 'design']);
|
|
|
16
16
|
const FINDING_CODE = /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)+$/;
|
|
17
17
|
const ENFORCER_ID = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
18
18
|
const FILE_ENFORCERS = new Set(['machine', 'hfs', 'work-validate', 'sonar', 'runtime']);
|
|
19
|
-
const RULE_KEYS = new Set(['id', 'code', 'law', 'scope', 'kinds', 'gates', 'failureCodes', 'editions', 'codeEditions', 'enforcers', 'cases', 'docs', 'exemplifiedBy', 'proves', 'witness', 'editionWhy', 'amendedBy', 'templateNa']);
|
|
19
|
+
const RULE_KEYS = new Set(['id', 'code', 'law', 'scope', 'kinds', 'gates', 'failureCodes', 'editions', 'codeEditions', 'enforcers', 'cases', 'docs', 'exemplifiedBy', 'proves', 'witness', 'editionWhy', 'amendedBy', 'templateNa', 'specNa', 'exampleNa']);
|
|
20
20
|
const CASE_KEYS = new Set(['id', 'date', 'source', 'ruling', 'quote', 'supersedes', 'status', 'staleWhen', 'amendedBy']);
|
|
21
21
|
const CASE_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
22
22
|
const ENFORCER_KEYS = new Set(['kind', 'id', 'status', 'at', 'editions', 'editionWhy']);
|
|
@@ -279,6 +279,8 @@ function checkRule(rule, index, catalog, codeOwner, bad) {
|
|
|
279
279
|
if (rule.amendedBy !== undefined) caseReferenceProblems(rule.amendedBy, `${label}.amendedBy`, bad);
|
|
280
280
|
optionalText(rule, 'editionWhy', label, bad);
|
|
281
281
|
optionalText(rule, 'templateNa', label, bad);
|
|
282
|
+
optionalText(rule, 'specNa', label, bad);
|
|
283
|
+
optionalText(rule, 'exampleNa', label, bad);
|
|
282
284
|
for (const key of ['docs', 'exemplifiedBy', 'proves']) evidenceProblems(rule, key, label, bad);
|
|
283
285
|
witnessProblems(rule, label, bad);
|
|
284
286
|
enumListProblems(rule, label, 'kinds', RULE_KINDS, bad);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// is this path tracked isTracked(P) / trackingOf(P); the rule catalog (knowledge/hfs/rules.yaml, modules/schemas/hfs-rules.schema.yaml) loads through loadRuleCatalog / rules().
|
|
6
6
|
// Every check and lint rule of HFS reads knowledge/hfs/slots.yaml through this module; none keeps its own path
|
|
7
7
|
// list. The manifest shape is modules/schemas/hfs-slots.schema.yaml and hfs.json is modules/schemas/hfs-repo.schema.yaml;
|
|
8
|
-
// the installed runtime carries no npm dependency, so this file re-states those shapes instead of loading ajv
|
|
8
|
+
// the installed runtime carries no npm dependency, so this file re-states those shapes instead of loading ajv.
|
|
9
9
|
//
|
|
10
10
|
// A product is ONE app repository: hfs.json at the app root has kind `app` and declares its two sides, `be` and `fe`, each in
|
|
11
11
|
// the folder of that name. The resolver of the app answers for the whole tree: a root path with the slots of profile `app`,
|
|
@@ -41,7 +41,7 @@ export { HfsSlotsError } from './slot-errors.mjs';
|
|
|
41
41
|
export { loadRuleCatalog };
|
|
42
42
|
export { rules } from './rule-catalog.mjs';
|
|
43
43
|
export { APP_SCOPE };
|
|
44
|
-
|
|
44
|
+
const HFS_MANIFEST_FILE = 'knowledge/hfs/slots.yaml';
|
|
45
45
|
/** The manifest of kind runtime: the standard tree of the StarCi runtime repository (judged by scripts/hfs/runtime-check.mjs). */
|
|
46
46
|
export const RUNTIME_MANIFEST_FILE = 'knowledge/hfs/runtime-slots.yaml';
|
|
47
47
|
export const HFS_DECLARATION_FILE = 'hfs.json';
|
|
@@ -9,14 +9,6 @@ import { byCodeUnit } from '../lib/list.mjs';
|
|
|
9
9
|
// builds afresh.
|
|
10
10
|
const runs = new AsyncLocalStorage();
|
|
11
11
|
|
|
12
|
-
/** A program run: run(fn) shares what is built inside fn; release() drops it. */
|
|
13
|
-
export function typeScriptProgramRun() {
|
|
14
|
-
const values = new Map();
|
|
15
|
-
return {
|
|
16
|
-
run: fn => runs.run(values, fn),
|
|
17
|
-
release: () => values.clear(),
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
12
|
|
|
21
13
|
function keyOf(value) {
|
|
22
14
|
if (value === undefined) return 'u';
|
|
@@ -11,8 +11,8 @@ export const isSpecRun = (env = process.env) => Boolean(env.NODE_TEST_CONTEXT);
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* The environment for a child that is itself a test runner: `env` without the mark of an enclosing spec run. A `node --test` that inherits NODE_TEST_CONTEXT believes it is a
|
|
14
|
-
* subtest of that run: it reports to its parent and exits 0 whatever its tests did, so a verb that runs specs as a child (
|
|
15
|
-
*
|
|
14
|
+
* subtest of that run: it reports to its parent and exits 0 whatever its tests did, so a verb that runs specs as a child (the package clean-test, the scaffold e2e, the release cut)
|
|
15
|
+
* would pass red specs from inside a test process. The runner marks the spec files it starts itself, so isSpecRun stays true inside them.
|
|
16
16
|
*/
|
|
17
17
|
export function withoutTestRunner(env = process.env) {
|
|
18
18
|
const { NODE_TEST_CONTEXT: _enclosing, ...rest } = env;
|
|
@@ -12,7 +12,7 @@ export const unquoteDiffPath = (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
|
-
// it core.bare=true (2026-09-29, a6f60352c).
|
|
15
|
+
// it core.bare=true (2026-09-29, a6f60352c). e2e/setup/isolated-registry.mjs drops the same list.
|
|
16
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) {
|
|
@@ -49,8 +49,6 @@ export function loadCatalog(root = skillRoot, { scope = 'runtime' } = {}) {
|
|
|
49
49
|
return cache.get(key);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
/** Forget the cached catalogs (a spec that rewrites a catalog file). */
|
|
53
|
-
export const resetCatalogCache = () => cache.clear();
|
|
54
52
|
|
|
55
53
|
/** The names each `pattern` match captures (group 1) in `text`, in match order (`sort` sorts them). */
|
|
56
54
|
export const captureNames = (text, pattern, { sort = false } = {}) => {
|
|
@@ -76,7 +76,7 @@ const I18N_CATALOG_DIR = 'modules/i18n/messages/';
|
|
|
76
76
|
const OP_MANIFEST_VIETNAMESE_FIELDS = Object.freeze({ fields: Object.freeze(['vi']) });
|
|
77
77
|
const OP_MANIFEST_DIR = 'modules/ops/ops/';
|
|
78
78
|
/** The declared Vietnamese fields of `rel`: its own entry, or the op-manifest text field for a manifest of modules/ops/ops. */
|
|
79
|
-
|
|
79
|
+
const declaredVietnameseFieldsOf = (rel) => DECLARED_VIETNAMESE_FIELDS[rel]
|
|
80
80
|
?? (rel.startsWith(I18N_CATALOG_DIR) && !rel.slice(I18N_CATALOG_DIR.length).includes('/') && /\.ya?ml$/.test(rel) ? I18N_CATALOG_VIETNAMESE_FIELDS : null)
|
|
81
81
|
?? (rel.startsWith(OP_MANIFEST_DIR) && !rel.slice(OP_MANIFEST_DIR.length).includes('/') && /\.ya?ml$/.test(rel) ? OP_MANIFEST_VIETNAMESE_FIELDS : null);
|
|
82
82
|
|