@starci/eslint-canon-fe 8.1.0 → 8.2.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 +4 -0
- package/package.json +1 -1
- package/runtime/config.example.yaml +7 -0
- package/runtime/engine/config.mjs +4 -3
- package/runtime/engine/release-config.mjs +28 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +4 -4
- package/runtime/modules/kernel/removed-vocabulary.yaml +100 -0
- package/runtime/modules/models/runtimes.yaml +38 -13
- package/runtime/modules/models/tiers.yaml +1 -0
- package/runtime/scripts/hfs/architecture/index.mjs +6 -2
- package/runtime/scripts/hfs/architecture/required-files.mjs +21 -2
- package/runtime/scripts/hfs/repo-identity.mjs +16 -2
- package/runtime/scripts/hfs/rule-catalog.mjs +2 -2
- package/runtime/scripts/hfs/slots.mjs +2 -2
- package/runtime/scripts/lib/yaml-cached.mjs +14 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 8.2.0 - 2026-10-09
|
|
4
|
+
|
|
5
|
+
- Changed: the bundled runtime copies (engine config, architecture machine, slot manifest, canon pins, failure codes) follow the runtime of 1.0.0-alpha.9; no rule is added or removed. Version 8.1.0 on the registry carries the copies of an earlier runtime.
|
|
6
|
+
|
|
3
7
|
## 8.1.0 - 2026-10-08
|
|
4
8
|
|
|
5
9
|
- New: the `sonar-parity` law (R235 `SCAN_SMELL`): `no-await-in-loop` (S9382), `prefer-code-point` (S7758), `prefer-string-raw` (S7780), `no-nested-conditional` (S3358), `no-void-operator` (S3735), `no-unused-import` (S1128), `prefer-export-from` (S7763) and `no-unused-prop-types` (S6767), each the syntax-tree form of the Sonar rule SonarCloud flagged in the example apps, shared with the back-end canon through the bundled `runtime/scripts/lib/sonar-syntax-rules.mjs`. The factory also turns on the typescript-eslint rules `no-deprecated` (S1874), `no-base-to-string` (S6551) and `require-await` (S7503); `@typescript-eslint/eslint-plugin` becomes a dependency of this package.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starci/eslint-canon-fe",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.2.0",
|
|
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,3 +185,10 @@ 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 — where the full test suite 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 suite and the Linux parity container before it pushes, and the pre-push gate asks for their green rows
|
|
190
|
+
# suite: ci the owner's recorded choice (2026-10-09): no local full suite and no Linux container; the GitHub ci workflow runs the suite (with coverage, Codecov, SonarCloud) AFTER the push.
|
|
191
|
+
# The cut still runs npm run check, npm run test:packages, the specs affected by the release, the live Orca smokes and the example apps; the record lists the root suite as
|
|
192
|
+
# delegated, never green. A release can reach GitHub and its tag before the suite has run; a red CI is fixed forward with the next pre-release (starci release ci-status).
|
|
193
|
+
# "none" is refused: the suite does not vanish, it moves to CI. Set it only in the gitignored config.yaml copy: release: {suite: ci}
|
|
194
|
+
release: {suite: local}
|
|
@@ -6,6 +6,7 @@ import {parseYaml} from './yaml.mjs';
|
|
|
6
6
|
import {isPlainObject as plain} from './plain-object.mjs';
|
|
7
7
|
import {invalid,validateRoots,ROOT_KEYS} from './invalid-config.mjs';
|
|
8
8
|
import {validateSonar} from './sonar-config.mjs';
|
|
9
|
+
import {validateRelease,RELEASE_KEYS} from './release-config.mjs';
|
|
9
10
|
import {validateOrca,ORCA_KEYS} from './orca-config.mjs';
|
|
10
11
|
import {validateResources,RESOURCE_KEYS} from './resources-config.mjs';
|
|
11
12
|
import {ENV_NAME,secretEnv,connectorSecret} from './secrets.mjs';
|
|
@@ -242,7 +243,7 @@ export function uatSettings(config=loadConfig()){
|
|
|
242
243
|
const GRANT=/^([a-z0-9][a-z0-9.-]*)=(\d+)@([a-z]+(?:\+[a-z]+)*)$/;
|
|
243
244
|
/** One grant string `<pool>=<slots>@<role>+<role>` as {pool, slots, roles}, or null when it is not that shape. */
|
|
244
245
|
/** The closed set of top-level config.yaml blocks: the validator accepts these and refuses every other key. */
|
|
245
|
-
export 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','launchTrust','retention']);
|
|
246
|
+
export 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']);
|
|
246
247
|
export function parseAllocationGrant(text){
|
|
247
248
|
const m=typeof text==='string'?GRANT.exec(text.trim()):null;
|
|
248
249
|
return m?{pool:m[1],slots:Number(m[2]),roles:m[3].split('+')}:null;
|
|
@@ -323,7 +324,7 @@ const validateEarlyConfigBlocks=(config)=>{
|
|
|
323
324
|
if(config?.launchTrust!==undefined){launchTrustSettings(config);} if(config?.retention!==undefined){workflowPurgeSettings(config);}
|
|
324
325
|
if(config?.connectors!==undefined){validateConnectors(config.connectors);} if(config?.asks!==undefined){validateAsks(config.asks);}
|
|
325
326
|
if(config?.uat!==undefined){validateUat(config.uat);} if(config?.debugLoop!==undefined){validateDebugLoop(config.debugLoop);}
|
|
326
|
-
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);}
|
|
327
|
+
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);}
|
|
327
328
|
};
|
|
328
329
|
const validateConfigBlocks=(config,knownProviders,runtimes,profile)=>{
|
|
329
330
|
// specs (owner 2026-09-28): {harness?, unit?, e2e?} booleans - each family a boolean; absent = its default (SPEC_DEFAULTS: harness off, unit on, e2e off; specsSettings).
|
|
@@ -416,7 +417,7 @@ export const CONFIG_KEY_TREE=Object.freeze({
|
|
|
416
417
|
supervisor:Object.freeze(['mode','pollIntervalMs','repos','stallMinutes','kernel','workers','landGate','frozenMinutes']),
|
|
417
418
|
delegation:Object.freeze(['asks','until','excludes','note']),asks:Object.freeze(['autoAcceptRecommended','excludes']),uat:Object.freeze(['maxConcurrent']),
|
|
418
419
|
specs:SPEC_FAMILIES,reconciler:Object.freeze(['enabled','profile','controllers']),debugLoop:Object.freeze(['interval','worktreeLimit']),
|
|
419
|
-
orca:ORCA_KEYS,roots:ROOT_KEYS,resources:RESOURCE_KEYS,
|
|
420
|
+
orca:ORCA_KEYS,roots:ROOT_KEYS,resources:RESOURCE_KEYS,release:RELEASE_KEYS,
|
|
420
421
|
});
|
|
421
422
|
export const SPEC_DEFAULTS=Object.freeze({harness:false,unit:true,e2e:false});
|
|
422
423
|
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]]));}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {isPlainObject as plain} from './plain-object.mjs';
|
|
2
|
+
import {invalid} from './invalid-config.mjs';
|
|
3
|
+
|
|
4
|
+
/** The keys of the release block. */
|
|
5
|
+
export const RELEASE_KEYS=Object.freeze(['suite']);
|
|
6
|
+
/** Where the full test suite of a release is judged: `local` (the release cut runs it, the shipped default) or `ci` (the GitHub workflow runs it after the push; the cut runs only the affected specs of the release and the rows CI cannot run). */
|
|
7
|
+
export const SUITE_MODES=Object.freeze(['local','ci']);
|
|
8
|
+
export const RELEASE_DEFAULTS=Object.freeze({suite:'local'});
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* config.yaml `release` - {suite?: local | ci}: who judges the full suite of a release (docs/releasing.md "Where the suite runs"). `local` is the shipped
|
|
12
|
+
* default: `starci release cut` runs the root suite and the Linux parity container before it pushes. `ci` is the owner's recorded choice (2026-10-09) that
|
|
13
|
+
* the full suite runs on GitHub only: the cut runs no root suite and no Linux container, and the CI workflow's verdict is read afterwards (`starci release ci-status`).
|
|
14
|
+
* `none` is refused: the suite does not vanish, it moves to CI, and the value says so.
|
|
15
|
+
*/
|
|
16
|
+
export function validateRelease(release){
|
|
17
|
+
if(release===null)return;
|
|
18
|
+
const bad=invalid('release');
|
|
19
|
+
if(!plain(release))bad(` must be {suite?: ${SUITE_MODES.join(' | ')}} or null.`);
|
|
20
|
+
for(const key of Object.keys(release))if(!RELEASE_KEYS.includes(key))bad(` has unknown key ${key} (allowed: ${RELEASE_KEYS.join(', ')}).`);
|
|
21
|
+
const value=release.suite;
|
|
22
|
+
if(value===undefined||value===null)return;
|
|
23
|
+
if(value==='none')bad('.suite: "none" is not a mode: write "ci" (no local full suite, CI judges) or "local".');
|
|
24
|
+
if(!SUITE_MODES.includes(value))bad(`.suite must be ${SUITE_MODES.join(' or ')}, or null.`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The suite mode a validated owner config names; an absent block or key is the shipped default. */
|
|
28
|
+
export const releaseSuiteMode=config=>config?.release?.suite??RELEASE_DEFAULTS.suite;
|
|
@@ -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 and the IconButton disclosure props. The runtime source version wins for a @starci package.'
|
|
31
31
|
'@starci/eslint-canon-be':
|
|
32
|
-
version: 3.
|
|
32
|
+
version: 3.2.0
|
|
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.
|
|
39
|
+
version: 8.2.0
|
|
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.
|
|
79
|
+
version: 1.2.0
|
|
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.
|
|
86
|
+
version: 4.2.0
|
|
87
87
|
group: starci
|
|
88
88
|
install: registry
|
|
89
89
|
side: both
|
|
@@ -154,6 +154,58 @@ removed:
|
|
|
154
154
|
use: 'skills/starci/references/host-startup.md'
|
|
155
155
|
since: 1.0.0-alpha.6
|
|
156
156
|
match: '(?<![\w-])host-maintenance\b'
|
|
157
|
+
- id: config-drawloop-critic
|
|
158
|
+
kind: config-key
|
|
159
|
+
name: allocation.drawLoop.critic
|
|
160
|
+
use: 'the Critic is picked from its tier (modules/models/tiers.yaml seats.critic, modules/kernel/critic.yaml) with provider independence as a hard filter; its wall bound is allocation.drawLoop.criticTimeoutMs'
|
|
161
|
+
since: 1.0.0-alpha.9
|
|
162
|
+
match: 'allocation\.drawLoop\.critic(?![A-Za-z])'
|
|
163
|
+
- id: config-drawloop-criticwhendrawer
|
|
164
|
+
kind: config-key
|
|
165
|
+
name: allocation.drawLoop.criticWhenDrawer
|
|
166
|
+
use: 'the Critic tier holds a member of every provider and the picker removes the provider of the maker; there is no per-drawer table'
|
|
167
|
+
since: 1.0.0-alpha.9
|
|
168
|
+
match: 'criticWhenDrawer'
|
|
169
|
+
- id: registry-critic-coverage-define-architecture
|
|
170
|
+
kind: vocabulary
|
|
171
|
+
name: critic-coverage-define-architecture
|
|
172
|
+
use: 'the edge-case registry entry is critic-coverage-decision-legs'
|
|
173
|
+
since: 1.0.0-alpha.9
|
|
174
|
+
- id: supervisor-grammar-release
|
|
175
|
+
kind: vocabulary
|
|
176
|
+
name: grammarRelease
|
|
177
|
+
use: 'the owner publishes @starci/grammar through starci release publish; the Supervisor changes no runtime code'
|
|
178
|
+
since: 1.0.0-alpha.9
|
|
179
|
+
- id: supervise-yaml-worker-land-blocks
|
|
180
|
+
kind: vocabulary
|
|
181
|
+
name: supervise.yaml workers
|
|
182
|
+
use: 'the Supervisor spawns no fix workers and lands nothing: it answers its menu (starci supervisor status) and records runtime defects for Debug'
|
|
183
|
+
since: 1.0.0-alpha.9
|
|
184
|
+
match: 'supervise\.yaml\s+(?:workers|landGate)\b|supervise\.yaml\s+kernelSeat/workers'
|
|
185
|
+
- id: supervisor-fixes-through-lanes
|
|
186
|
+
kind: vocabulary
|
|
187
|
+
name: the Supervisor fixes through lanes
|
|
188
|
+
use: 'the Supervisor records a runtime defect (starci supervisor actions record --item runtime-defect:<cause>) and Debug changes .claude'
|
|
189
|
+
since: 1.0.0-alpha.9
|
|
190
|
+
match: 'Supervisor[^\n.]{0,60}\bfix(?:es)?\s+through\s+(?:its\s+own\s+)?(?:staged\s+)?lanes?\b'
|
|
191
|
+
- id: kernel-ranked-actions
|
|
192
|
+
kind: vocabulary
|
|
193
|
+
name: ranked actions
|
|
194
|
+
use: "the Kernel's menu: the Decide section of starci kernel status, answered with starci kernel decide"
|
|
195
|
+
since: 1.0.0-alpha.9
|
|
196
|
+
match: '[Ww]ork the ranked actions|[Ff]irst rca\.actions entry'
|
|
197
|
+
- id: kernel-run-next-actions
|
|
198
|
+
kind: vocabulary
|
|
199
|
+
name: run nextActions
|
|
200
|
+
use: "the Kernel answers the items of its menu; the runtime performs the mechanical next actions"
|
|
201
|
+
since: 1.0.0-alpha.9
|
|
202
|
+
match: 'run nextActions|run each\s+in order and never choose'
|
|
203
|
+
- id: event-op-reported
|
|
204
|
+
kind: vocabulary
|
|
205
|
+
name: op-reported
|
|
206
|
+
use: 'the event a filed report records is report-filed; no event named op-reported was ever emitted'
|
|
207
|
+
since: 1.0.0-alpha.9
|
|
208
|
+
match: '(?<![\w-])op-reported(?![\w-])'
|
|
157
209
|
- id: file-sonar-secrets-keys
|
|
158
210
|
kind: file
|
|
159
211
|
name: KEYS.md
|
|
@@ -166,3 +218,51 @@ removed:
|
|
|
166
218
|
use: 'the examples are analysed on SonarCloud with the SONAR_TOKEN of the runtime secret.env (runtimeSecrets in their services.sonar declaration)'
|
|
167
219
|
since: 1.0.0-alpha.7
|
|
168
220
|
match: 'sonarqube-(?:lite-app|shape-slot|starci-ecommerce-app)-token'
|
|
221
|
+
- id: vocab-menu-kinds-moved-to-controllers
|
|
222
|
+
kind: vocabulary
|
|
223
|
+
name: the menu kinds of the origins the controllers now move
|
|
224
|
+
use: 'the Job controller enqueues a red node, a stale proof or attempt, the asset leg and the credential ask itself (scripts/kernel/next-moves.mjs); the Kernel menu keeps leg-ready, draw-redraw, seam-duty and move-unbuilt'
|
|
225
|
+
since: 1.0.0-alpha.9
|
|
226
|
+
match: '(?<![\w-])(?:node-rework|stale-redispatch|enqueue-asset-leg|enqueue-credential-ask|rework-node)(?![\w-])'
|
|
227
|
+
- id: vocab-sonar-stack-sealed-members
|
|
228
|
+
kind: vocabulary
|
|
229
|
+
name: ext/sonar secrets custody
|
|
230
|
+
use: 'the stack secrets are the secret.env variables SONARQUBE_DB_PASSWORD, SONARQUBE_ADMIN_PASSWORD, SONARQUBE_ADMIN_TOKEN and CLOUDFLARE_TUNNEL_TOKEN, passed by starci gate sonar up; ext/sonar tracks no sealed member'
|
|
231
|
+
since: 1.0.0-alpha.9
|
|
232
|
+
match: 'ext/sonar/secrets/(?:sonarqube-(?:db|admin|analysis)-(?:password|token)|cloudflare-starci-local-services-tunnel-token)'
|
|
233
|
+
- id: vocab-ecommerce-sealed-demo-secrets
|
|
234
|
+
kind: vocabulary
|
|
235
|
+
name: ecommerce sealed demo secrets
|
|
236
|
+
use: 'the ecommerce example carries demo-only defaults in its compose files and declares no sealed secret (secrets: [])'
|
|
237
|
+
since: 1.0.0-alpha.9
|
|
238
|
+
match: '(?<![\w-])(?:keycloak|minio)-env(?:\.enc)?(?![\w-])'
|
|
239
|
+
- id: vocab-handover-step-mechanical-pending
|
|
240
|
+
kind: vocabulary
|
|
241
|
+
name: the handover-step menu mode mechanical-pending and its choice rerun-handover-review
|
|
242
|
+
use: 'the Job controller enqueues handover.review itself when the handover is due or its ask was answered approve or question (next action handover-review, scripts/kernel/handover-move.mjs); the handover-step item holds only the typed routing of a reported defect'
|
|
243
|
+
since: 1.0.0-alpha.9
|
|
244
|
+
match: '(?<![\w-])(?:mechanical-pending|rerun-handover-review)(?![\w-])'
|
|
245
|
+
- id: vocab-card-delivery-bound
|
|
246
|
+
kind: vocabulary
|
|
247
|
+
name: the agent card delivery keys maxInlineChars, fileName and fileDirectory
|
|
248
|
+
use: 'one bound, one directory and one cleanup serve every card: allocation.promptFile in modules/models/runtimes.yaml (scripts/agent/prompt-file.mjs); a card keeps delivery.mode and delivery.prompt'
|
|
249
|
+
since: 1.0.0-alpha.9
|
|
250
|
+
match: '(?<![\w-])(?:maxInlineChars|fileDirectory)(?![\w-])'
|
|
251
|
+
- id: vocab-backoff-factor-keys
|
|
252
|
+
kind: vocabulary
|
|
253
|
+
name: the growth factor of allocation.dispatchRefusal and of the backoff of modules/reconciler/host.yaml
|
|
254
|
+
use: 'every backoff is the one retry budget (scripts/lib/retry-budget.mjs), which doubles its interval up to a cap: declare baseMs/capMs or minMs/maxMs only'
|
|
255
|
+
since: 1.0.0-alpha.9
|
|
256
|
+
match: '(?<![\w-])(?:dispatchRefusal|backoff):\s*\{[^}\n]*\bfactor\b'
|
|
257
|
+
- id: vocab-wake-terminal-bound
|
|
258
|
+
kind: vocabulary
|
|
259
|
+
name: the independent wake character bound
|
|
260
|
+
use: 'allocation.promptFile.maxChars is the terminal bound owned by scripts/agent/prompt-file.mjs'
|
|
261
|
+
since: 1.0.0-alpha.9
|
|
262
|
+
match: '(?<![\w-])(?:allocation\.)?wake\.maxChars|\bwake:\s*\{\s*maxChars\b'
|
|
263
|
+
- id: vocab-draft-probe-mutations
|
|
264
|
+
kind: vocabulary
|
|
265
|
+
name: draft probe mutation fields
|
|
266
|
+
use: 'probeDraft reads only and returns verdict, draft and sends; a foreign draft receives no keys'
|
|
267
|
+
since: 1.0.0-alpha.9
|
|
268
|
+
match: '\b(?:draftProbe|probeDraft)\.(?:restore|restored|removed|after)\b'
|
|
@@ -93,9 +93,20 @@ allocation:
|
|
|
93
93
|
# the decision doorbell; still the same turn turnInterruptGraceMs later, the seat is replaced (KERNEL_TURN_OVERDUE,
|
|
94
94
|
# scripts/reconciler/controllers/host.mjs turnStep). A busy turn defers every doorbell and notice (kernel-busy).
|
|
95
95
|
liveness: {activeUnclassifiedMs: 120000, activeStaleMs: 600000, quietMs: 1200000, launchGraceMs: 90000,
|
|
96
|
-
kernelTurnBudgetMs: 1200000, supervisorTurnBudgetMs: 1800000, turnInterruptGraceMs: 300000
|
|
96
|
+
kernelTurnBudgetMs: 1200000, supervisorTurnBudgetMs: 1800000, turnInterruptGraceMs: 300000,
|
|
97
|
+
# The watchdog's replacement of a Kernel seat runs start-workflow (worktree check, install, Orca launch, attestation): bounded here, under the Host controller's
|
|
98
|
+
# seats.kernel.timeoutMs that kills the whole watchdog pass. A start that outlives it is killed and recorded kernel-start-failed (start-timeout), never lost silently.
|
|
99
|
+
kernelStartTimeoutMs: 600000}
|
|
97
100
|
# one local git read over a job's owned paths (scripts/kernel/owned-path-effects.mjs ownedPathEffects, job-artifacts.mjs).
|
|
98
101
|
settleGit: {commandMs: 15000}
|
|
102
|
+
# prompt-file.mjs owns terminal delivery, wake bounds, file placement and cleanup.
|
|
103
|
+
promptFile: {maxChars: 2000, ttlMs: 86400000}
|
|
104
|
+
# opVerbs: the bound of the "your verbs" block of an [Op] prompt (scripts/kernel/op-prompt-verbs.mjs): the starci calls the op's contract names,
|
|
105
|
+
# each with the flags the contract uses and the required ones, one line per verb. A worker that has the exact call never runs --help
|
|
106
|
+
# (a measured op spent 15 of its 135 turns on it). `standing` verbs are printed in full by the prompt's own reporting and logging blocks;
|
|
107
|
+
# `omitGroups` are the verbs of the Kernel and above, which an op's contract names only to say who runs them.
|
|
108
|
+
opVerbs: {maxVerbs: 20, maxChars: 3000, lineChars: 190, standing: ["kernel report", "kernel log", "kernel op-contract"],
|
|
109
|
+
omitGroups: [kernel, supervisor, workflow, reconciler]}
|
|
99
110
|
# Typed logs (scripts/kernel/typed-logs.mjs, the ledger's logs table): perJobCap rows a job's own writes
|
|
100
111
|
# may store before one log.truncated row closes them (derived event rows are never capped); dataMaxBytes bounds
|
|
101
112
|
# one row's data after redaction. diff: the caps of a job patch's pre-structured <patch>.json
|
|
@@ -187,6 +198,13 @@ allocation:
|
|
|
187
198
|
# replacement takes over the loop's lock within handoverMs or is stopped. The reconciler engine checks every checkMs; an engine whose
|
|
188
199
|
# revision still differs from the live one that long past minIntervalMs + checkMs + handoverMs is a drift row (scripts/reconciler/drift.mjs).
|
|
189
200
|
selfReload: {minIntervalMs: 300000, handoverMs: 30000, checkMs: 60000, driftGraceMs: 120000}
|
|
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. The affected-spec child
|
|
203
|
+
# may run its declared budget (modules/supervisor/affected-tests.yaml) plus affectedMarginMs before the deploy stops it.
|
|
204
|
+
deploy: {waitMs: 1800000, pollMs: 5000, verifyMs: 120000, affectedMarginMs: 300000}
|
|
205
|
+
# 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
|
+
# the cause's fingerprint (runtime revision, a ruling, a resource the refusal names) tries it at once.
|
|
207
|
+
dispatchRefusal: {baseMs: 60000, capMs: 900000}
|
|
190
208
|
# footprint (scripts/guards/footprint-scan.mjs): one host-wide scan at most every everyMs; a crashed
|
|
191
209
|
# tick's claim lock is cleared after lockStaleMs.
|
|
192
210
|
footprint: {everyMs: 600000, lockStaleMs: 60000}
|
|
@@ -194,12 +212,15 @@ allocation:
|
|
|
194
212
|
jobGuard: {ttlMs: 604800000}
|
|
195
213
|
# landGate (scripts/supervisor/land.mjs): a land waits waitMs for its turn at the gate; its spec run may take
|
|
196
214
|
# specsBaseMs plus perSpecMs for every spec it runs, specConcurrency spec files at a time.
|
|
197
|
-
landGate: {waitMs: 1800000, specsBaseMs: 600000, perSpecMs: 60000, specConcurrency: 6}
|
|
215
|
+
landGate: {waitMs: 1800000, specsBaseMs: 600000, perSpecMs: 60000, specConcurrency: 6, cliVerbs: 12}
|
|
198
216
|
# workerClose (scripts/machine/worker-close.mjs; owner rule: a finished worker is closed COMPLETELY): after worker-release and the terminal
|
|
199
217
|
# close the runtime waits verifyMs for every process of that terminal's shell tree to end (polled every pollMs); a survivor proven to belong
|
|
200
218
|
# to the tree is stopped and given stopVerifyMs to end before the finding is raised.
|
|
201
219
|
# A finished [Worker] job whose terminal has no recorded closure is closed again by the Job controller's worker sweep (scripts/supervisor/supervisor-watchdog.mjs):
|
|
202
220
|
# every leftoverRetryMs (doubling to leftoverRetryMaxMs) for at most leftoverRetryAttempts attempts, each failure recorded with its reason on the job.
|
|
221
|
+
# uatSlot (scripts/uat/slot-collect.mjs, slot-lessee.mjs): a held UAT run checks its lessee (its attempt, the process that launched it) every watchMs and ends with it;
|
|
222
|
+
# the Host pass ends a slot whose lessee is gone, and a slot that recorded no lessee at all once it has been held unknownHoldMs (3 hours: a UAT run is minutes).
|
|
223
|
+
uatSlot: {watchMs: 5000, unknownHoldMs: 10800000}
|
|
203
224
|
workerClose: {verifyMs: 10000, pollMs: 1000, stopVerifyMs: 5000, leftoverRetryMs: 60000, leftoverRetryMaxMs: 900000, leftoverRetryAttempts: 12}
|
|
204
225
|
# providerReservation (scripts/machine/provider-reservation-reap.mjs): an admission whose receipt stayed `reserved` this long never reached
|
|
205
226
|
# worker-start (the consume moves it to `launching` first), so its launcher died and the slot is released. A receipt with no terminal handle and no
|
|
@@ -238,20 +259,26 @@ allocation:
|
|
|
238
259
|
quitAgent: {waitMs: 6000}
|
|
239
260
|
# drawRender (scripts/work/draw-render.mjs): a capture waits settleMs after document.fonts.ready.
|
|
240
261
|
drawRender: {settleMs: 500}
|
|
262
|
+
# layoutRender (scripts/work/layout-render.mjs): the runtime serves the product app for one capture; readyMs is how long the dev server may take to answer
|
|
263
|
+
# its first request (a cold Next compile), navigateMs the page load, settleMs the wait after fonts are ready, stopMs the grace before a still-running server is killed.
|
|
264
|
+
# publicOriginKeys are the scaffold's public-origin variables (its README: the product app's own address and the product app the landing hands over to); each gets the render's origin.
|
|
265
|
+
layoutRender: {readyMs: 180000, navigateMs: 90000, settleMs: 500, stopMs: 5000, publicOriginKeys: [NEXT_PUBLIC_SITE_URL, NEXT_PUBLIC_APP_URL]}
|
|
241
266
|
# drawLoop (scripts/work/draw-loop.mjs, scripts/work/draw/draw-taste.mjs; owner rulings 2026-09-27): interface.draw
|
|
242
267
|
# draws -> shoots -> evaluates -> fixes in rounds. A loop stops when every machine metric passes and the
|
|
243
268
|
# independent critic's beauty is at least beautyMin, after maxRounds, or after stallRounds rounds in a row that
|
|
244
269
|
# neither cut the failures nor raised the beauty; the best round is kept. Taste thresholds: accentBudget is the
|
|
245
270
|
# largest share of the render the accent colour may fill (the brand art band excluded), bandsPerCardMax the most
|
|
246
271
|
# hairline bands one card splits into, badgesPerEntityMax the most badges one entity carries. critic is the fresh,
|
|
247
|
-
# context-free Orca worker that scores beauty and hierarchy against the product's brand.direction rubric:
|
|
272
|
+
# context-free Orca worker that scores beauty and hierarchy against the product's brand.direction rubric: picked through
|
|
273
|
+
# the tier picker from the Critic tier (modules/models/tiers.yaml seats.critic, modules/kernel/critic.yaml) with the
|
|
274
|
+
# independence rule as a hard filter, then started
|
|
248
275
|
# through orchestration worker-start --agent <provider> --model --effort (scripts/agent/lib.mjs startAgent) on a
|
|
249
276
|
# runtime worktree detached at the empty tree (draw-critic.mjs criticWorkspace; Orca refuses a bare temp dir) holding only the PNGs, the HTML and the rubric; it writes verdict.json and reports worker_done; a
|
|
250
|
-
# critic with no worker_done within
|
|
251
|
-
#
|
|
252
|
-
# (
|
|
253
|
-
#
|
|
254
|
-
#
|
|
277
|
+
# critic with no worker_done within criticTimeoutMs is stopped and released (outcome timeout, no beauty). The critic is a member of ANOTHER PROVIDER than
|
|
278
|
+
# the drawer: the members of its tier of that provider are removed before the pick, and a tier with no other provider's member refuses the critique with
|
|
279
|
+
# CRITIC_NO_INDEPENDENT_MEMBER (the drawer never judges itself). The images a drawer asks of the imagegen call
|
|
280
|
+
# (starci work imagegen) do not make the call's model the drawer. The drawer is the op's provider, bound to its
|
|
281
|
+
# terminal (scripts/guards/op-context.mjs) or named by draw-loop.mjs round --drawer; a round with no independent critic has no beauty. directionPrerequisite: interface.draw needs an ACCEPTED brand.direction
|
|
255
282
|
# archetype for its ui record's ui.archetype - brand.mjs checkDirection evidence.ready, the owner's receipt
|
|
256
283
|
# (scripts/kernel/prerequisites.mjs direction-unaccepted; the Kernel enqueues brand.decide --param
|
|
257
284
|
# directionArchetype=<archetype> first). false switches the gate off.
|
|
@@ -262,9 +289,7 @@ allocation:
|
|
|
262
289
|
accentBudget: 0.10
|
|
263
290
|
bandsPerCardMax: 3
|
|
264
291
|
badgesPerEntityMax: 2
|
|
265
|
-
|
|
266
|
-
criticWhenDrawer:
|
|
267
|
-
codex: {provider: claude, model: claude-opus-5-5, effort: high, timeoutMs: 900000}
|
|
292
|
+
criticTimeoutMs: 900000
|
|
268
293
|
directionPrerequisite: true
|
|
269
294
|
# How long the workflow that claimed foundation `shell` (the layout chain an interface.draw draws) may hold it before a
|
|
270
295
|
# waiting draw opens a Supervisor Decision Item (scripts/kernel/shell-foundation.mjs; owner ruling 2026-09-29 draw-from-todo).
|
|
@@ -303,8 +328,8 @@ allocation:
|
|
|
303
328
|
# reportWindowMs.
|
|
304
329
|
workerJobs: {leaseTtlMs: 604800000, reportWindowMs: 604800000}
|
|
305
330
|
# draft (scripts/kernel/clear-draft.mjs): the beat between one Ctrl+U and the re-read that judges
|
|
306
|
-
# the input box's answer - a TUI repaints a beat later. clearDraft
|
|
307
|
-
#
|
|
331
|
+
# the input box's answer - a TUI repaints a beat later. clearDraft defaults to it and
|
|
332
|
+
# wake-delivery caps its runtime-owned draft clear interval at it.
|
|
308
333
|
draft: {intervalMs: 300}
|
|
309
334
|
# housekeeping (scripts/housekeeping/housekeeping.mjs): every retention window and root the host
|
|
310
335
|
# cleanup reads - no literal may live in the script. tmp: top-level %TEMP% entries whose name starts
|
|
@@ -14,7 +14,7 @@ import { buildHfsGraph } from './hfs-graph.mjs';
|
|
|
14
14
|
import { checkTiers, TIER_RULE_IDS } from './tiers.mjs';
|
|
15
15
|
import { checkReachability, REACHABILITY_RULE_IDS } from './reachability.mjs';
|
|
16
16
|
import { checkDeadExports, DEAD_EXPORT_RULE_IDS } from './dead-exports.mjs';
|
|
17
|
-
import { checkRequiredFiles, REQUIRED_FILE_RULE_IDS } from './required-files.mjs';
|
|
17
|
+
import { checkRequiredFiles, REQUIRED_FILE_RULE_IDS, withTreeCache } from './required-files.mjs';
|
|
18
18
|
import { checkClones, CLONE_RULE_IDS } from './clones.mjs';
|
|
19
19
|
import { checkSymbols, SYMBOL_RULE_IDS } from './symbols.mjs';
|
|
20
20
|
import { checkConnectionMap, CONNECTION_RULE_IDS } from './connection-map.mjs';
|
|
@@ -321,7 +321,11 @@ function scopeFindings({ config, context, violations, paths, surface }) {
|
|
|
321
321
|
* Check a target repository. injectedTypeScript exists only for hermetic rule fixtures. `fast` leaves out the checks
|
|
322
322
|
* that read the whole repository to answer (clones, dead exports, repository-wide symbols); the pre-push check of the changed owners uses it.
|
|
323
323
|
*/
|
|
324
|
-
export function checkArchitecture(
|
|
324
|
+
export function checkArchitecture(options = {}) {
|
|
325
|
+
return withTreeCache(() => checkArchitectureOnce(options));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function checkArchitectureOnce({ repositoryRoot, injectedTypeScript, paths = [], base, fast = false, hfs: openedHfs, surface = 'all' }) {
|
|
325
329
|
if (!['all', 'lint', 'check'].includes(surface)) throw new Error(`unknown surface ${surface}`);
|
|
326
330
|
let config;
|
|
327
331
|
try {
|
|
@@ -49,8 +49,20 @@ function diskFiles(root, relative = '') {
|
|
|
49
49
|
return out;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
let runTrees = null;
|
|
53
|
+
|
|
54
|
+
/** Runs `run` with one tree read per root: every checker of one machine run shares the repository listing instead of spawning git again. */
|
|
55
|
+
export function withTreeCache(run) {
|
|
56
|
+
const outer = runTrees;
|
|
57
|
+
runTrees = outer ?? new Map();
|
|
58
|
+
try {
|
|
59
|
+
return run();
|
|
60
|
+
} finally {
|
|
61
|
+
runTrees = outer;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readTree(root) {
|
|
54
66
|
const list = gitFiles(root) ?? diskFiles(root);
|
|
55
67
|
const files = new Set(list);
|
|
56
68
|
const directories = new Set();
|
|
@@ -61,6 +73,13 @@ export function treeOf(root) {
|
|
|
61
73
|
return { files, directories };
|
|
62
74
|
}
|
|
63
75
|
|
|
76
|
+
/** The repository tree as file and directory sets over posix relatives. */
|
|
77
|
+
export function treeOf(root) {
|
|
78
|
+
if (!runTrees) return readTree(root);
|
|
79
|
+
if (!runTrees.has(root)) runTrees.set(root, readTree(root));
|
|
80
|
+
return runTrees.get(root);
|
|
81
|
+
}
|
|
82
|
+
|
|
64
83
|
/**
|
|
65
84
|
* The prelude every tree-walking architecture checker shares: the input's config/graph/context, the compiler,
|
|
66
85
|
* the slot resolver and the tracked repository tree.
|
|
@@ -14,14 +14,28 @@ const real = (p) => { try { return fs.realpathSync(p); } catch { return path.res
|
|
|
14
14
|
const GIT_SUFFIX_SOURCE = String.raw`\.git$`;
|
|
15
15
|
const GIT_SUFFIX = new RegExp(GIT_SUFFIX_SOURCE, 'iu');
|
|
16
16
|
|
|
17
|
+
// The identity of a folder that has its own .git entry holds while that entry is the same file system object (same inode, same modification time), so a
|
|
18
|
+
// process asks Git once per such folder state instead of at every check. A folder with no .git entry of its own is asked every time.
|
|
19
|
+
const known = new Map();
|
|
20
|
+
const stateOf = (root) => { try { const stat = fs.statSync(path.join(root, '.git')); return `${real(root)}|${stat.ino}|${stat.mtimeMs}`; } catch { return null; } };
|
|
21
|
+
|
|
17
22
|
/** { repositoryRoot, inWorkTree, home } for `root`: home is the main checkout folder when root is a repository top level. */
|
|
18
23
|
function identityOf(root) {
|
|
24
|
+
const state = stateOf(root);
|
|
25
|
+
if (state === null) return askGit(root);
|
|
26
|
+
if (!known.has(state)) known.set(state, askGit(root));
|
|
27
|
+
return known.get(state);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function askGit(root) {
|
|
19
31
|
let inWorkTree = false, repositoryRoot = false, home = null;
|
|
20
32
|
try {
|
|
21
33
|
// Identity comes from Git only when root IS a repository (or worktree) top level, never a folder inside one.
|
|
22
|
-
|
|
34
|
+
// One git call answers both questions: the top level on the first line, the common git dir on the second.
|
|
35
|
+
const [top, commonDir] = git(revParseQuery, root, ['--show-toplevel', '--git-common-dir']).split(String.fromCodePoint(10)).map((line) => line.trim());
|
|
36
|
+
repositoryRoot = real(top) === real(root);
|
|
23
37
|
inWorkTree = true;
|
|
24
|
-
const common = path.resolve(root,
|
|
38
|
+
const common = path.resolve(root, commonDir);
|
|
25
39
|
if (repositoryRoot && path.basename(common) === '.git') home = path.dirname(common);
|
|
26
40
|
} catch { /* Not a Git work tree; the caller falls back to the folder and package name. */ }
|
|
27
41
|
return { inWorkTree, repositoryRoot, home };
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { skillRoot } from '../../engine/runtime-root.mjs';
|
|
5
|
-
import {
|
|
5
|
+
import { parseYamlCached } from '../lib/yaml-cached.mjs';
|
|
6
6
|
import { isPlainObject } from '../../engine/plain-object.mjs';
|
|
7
7
|
import { SEMVER } from './manifest-shape.mjs';
|
|
8
8
|
import { enforcerJudgedInEdition, judgedInEdition, ruleEditionProblems } from './edition-slots.mjs';
|
|
@@ -153,7 +153,7 @@ const deepFreeze = (value) => { if (value && typeof value === 'object') { Object
|
|
|
153
153
|
*/
|
|
154
154
|
export function loadRuleCatalog({ root = skillRoot, file = path.join(root, HFS_RULES_FILE), text, manifest } = {}) {
|
|
155
155
|
let doc;
|
|
156
|
-
try { doc =
|
|
156
|
+
try { doc = parseYamlCached(text ?? fs.readFileSync(file, 'utf8')); } catch (error) { fail('HFS_RULES_INVALID', `the rule catalog cannot be read (${String(error?.message ?? error).split('\n')[0]})`, { file }); }
|
|
157
157
|
const problems = ruleCatalogProblems(doc);
|
|
158
158
|
if (problems.length) { fail('HFS_RULES_INVALID', `the rule catalog breaks its schema: ${problems.slice(0, 5).join('; ')}${problems.length > 5 ? '; and ' + (problems.length - 5) + ' more' : ''}`, { file, problems }); }
|
|
159
159
|
const [major, minor, patch] = doc.version.split('.').map(Number);
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import fs from 'node:fs';
|
|
22
22
|
import path from 'node:path';
|
|
23
23
|
import { skillRoot } from '../../engine/runtime-root.mjs';
|
|
24
|
-
import {
|
|
24
|
+
import { parseYamlCached } from '../lib/yaml-cached.mjs';
|
|
25
25
|
import { braceVariants } from '../lib/glob.mjs';
|
|
26
26
|
import { APP_KIND, RUNTIME_KIND, manifestKind } from './manifest-shape.mjs';
|
|
27
27
|
import { APP_SCOPE, manifestShapeProblems, PROFILES } from './slot-manifest-shape.mjs';
|
|
@@ -73,7 +73,7 @@ export function appRelativeMessages(side, sideRoot) {
|
|
|
73
73
|
*/
|
|
74
74
|
export function loadSlotManifest({ root = skillRoot, file = path.join(root, HFS_MANIFEST_FILE), text } = {}) {
|
|
75
75
|
let doc;
|
|
76
|
-
try { doc =
|
|
76
|
+
try { doc = parseYamlCached(text ?? fs.readFileSync(file, 'utf8')); } catch (error) { fail('HFS_MANIFEST_INVALID', `the slot manifest cannot be read (${String(error?.message ?? error).split('\n')[0]})`, { file }); }
|
|
77
77
|
const problems = manifestShapeProblems(doc);
|
|
78
78
|
if (!problems.length) problems.push(...manifestSemanticProblems(doc, { varsOf, braceVariants, compileVariant }));
|
|
79
79
|
if (problems.length) fail('HFS_MANIFEST_INVALID', `the slot manifest breaks its schema: ${problems.slice(0, 5).join('; ')}${problems.length > 5 ? '; and ' + (problems.length - 5) + ' more' : ''}`, { file, problems });
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// yaml-cached.mjs - parse a YAML text once per distinct text in a process.
|
|
2
|
+
//
|
|
3
|
+
// The HFS slot manifest and rule catalog are large documents that one process asks for again at every check, every render and every fixture. Their
|
|
4
|
+
// bytes do not change between those asks, so the parse (the dominant cost of a load) is keyed by the text itself; each caller gets its own copy of the
|
|
5
|
+
// parsed document, so nothing a caller does to it reaches the next one. A changed file is a different text and parses again.
|
|
6
|
+
import { parseYaml } from '../../engine/yaml.mjs';
|
|
7
|
+
|
|
8
|
+
const parsed = new Map();
|
|
9
|
+
|
|
10
|
+
/** The parsed document of `text`: parsed on the first ask for this text, a structured copy afterwards. Throws what parseYaml throws, every time. */
|
|
11
|
+
export function parseYamlCached(text) {
|
|
12
|
+
if (!parsed.has(text)) parsed.set(text, parseYaml(text));
|
|
13
|
+
return structuredClone(parsed.get(text));
|
|
14
|
+
}
|