@starci/eslint-grammar-core 0.15.34 → 0.16.2

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -1
  3. package/dist/index.js +4 -0
  4. package/dist/lib/why.js +15 -0
  5. package/dist/mascot.d.ts +28 -0
  6. package/dist/mascot.js +166 -0
  7. package/dist/surface-depth.d.ts +7 -0
  8. package/dist/surface-depth.js +92 -0
  9. package/docs/mascot.md +60 -0
  10. package/docs/surface-depth.md +40 -0
  11. package/package.json +2 -2
  12. package/runtime/bundle.json +74 -0
  13. package/runtime/engine/by-code-unit.mjs +6 -6
  14. package/runtime/engine/model-config.mjs +98 -76
  15. package/runtime/engine/orca-config.mjs +22 -17
  16. package/runtime/engine/release-config.mjs +22 -18
  17. package/runtime/engine/removed-vocabulary.mjs +21 -20
  18. package/runtime/engine/resources-config.mjs +42 -35
  19. package/runtime/engine/runtime-root.mjs +11 -13
  20. package/runtime/engine/secrets.mjs +170 -102
  21. package/runtime/engine/sonar-config.mjs +16 -13
  22. package/runtime/knowledge/hfs/canon-pins.yaml +10 -10
  23. package/runtime/knowledge/hfs/rules.yaml +95 -6
  24. package/runtime/knowledge/hfs/slots.yaml +2 -2
  25. package/runtime/knowledge/patterns/fe/ui.yaml +13 -1
  26. package/runtime/scripts/hfs/allows.mjs +37 -38
  27. package/runtime/scripts/hfs/architecture/config.mjs +302 -268
  28. package/runtime/scripts/hfs/architecture/side-boundary.mjs +2 -5
  29. package/runtime/scripts/hfs/architecture/type-context.mjs +255 -250
  30. package/runtime/scripts/hfs/architecture/typescript.mjs +358 -318
  31. package/runtime/scripts/hfs/declaration-shape.mjs +140 -103
  32. package/runtime/scripts/hfs/declaration-slots.mjs +64 -61
  33. package/runtime/scripts/hfs/edition-slots.mjs +43 -40
  34. package/runtime/scripts/hfs/param-names.mjs +6 -11
  35. package/runtime/scripts/hfs/rule-catalog.mjs +353 -257
  36. package/runtime/scripts/hfs/rule-params-shape.mjs +60 -47
  37. package/runtime/scripts/hfs/slot-app-view.mjs +69 -74
  38. package/runtime/scripts/hfs/slot-classify.mjs +42 -38
  39. package/runtime/scripts/hfs/slot-errors.mjs +8 -7
  40. package/runtime/scripts/hfs/slot-imports.mjs +70 -63
  41. package/runtime/scripts/hfs/slot-manifest-shape.mjs +74 -52
  42. package/runtime/scripts/hfs/slot-match.mjs +122 -111
  43. package/runtime/scripts/hfs/slot-path.mjs +0 -2
  44. package/runtime/scripts/hfs/slot-required.mjs +65 -59
  45. package/runtime/scripts/hfs/slot-semantic-problems.mjs +138 -112
  46. package/runtime/scripts/hfs/slot-side-problems.mjs +23 -19
  47. package/runtime/scripts/hfs/slots.mjs +175 -152
  48. package/runtime/scripts/hfs/typescript-programs.mjs +66 -69
  49. package/runtime/scripts/hfs/view.mjs +38 -40
  50. package/runtime/scripts/lib/glob.mjs +23 -14
  51. package/runtime/scripts/lib/grammar-lint.mjs +33 -37
  52. package/runtime/scripts/lib/i18n.mjs +32 -35
  53. package/runtime/scripts/lib/json-clone.mjs +45 -36
  54. package/runtime/scripts/lib/json.mjs +26 -14
  55. package/runtime/scripts/lib/list.mjs +9 -14
  56. package/runtime/scripts/lib/path-key.mjs +53 -38
  57. package/runtime/scripts/lib/read-text.mjs +30 -17
  58. package/runtime/scripts/lib/regex.mjs +1 -2
  59. package/runtime/scripts/lib/source-phrases.mjs +13 -13
  60. package/runtime/scripts/lib/ts-analysis.mjs +28 -26
  61. package/runtime/scripts/lib/ts-ast.mjs +36 -42
  62. package/runtime/scripts/lib/walk.mjs +33 -27
  63. package/runtime/scripts/lib/yaml-cached.mjs +4 -4
@@ -1,134 +1,202 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
-
4
3
  /** The host-local credential filename, resolved only against a verified runtime main root. */
5
- export const SECRET_ENV_FILE='secret.env';
4
+ export const SECRET_ENV_FILE = 'secret.env';
6
5
  /** The configured credential variable grammar shared by validation and secret resolution. */
7
- export const ENV_NAME=/^[A-Z_][A-Z0-9_]{0,63}$/;
8
- export const CREDENTIAL_FILE_MAX_BYTES=1024*1024;
6
+ export const ENV_NAME = /^[A-Z_][A-Z0-9_]{0,63}$/;
7
+ export const CREDENTIAL_FILE_MAX_BYTES = 1024 * 1024;
9
8
  const resolvedValues = new Set();
10
- const remember = value => {
11
- if (typeof value === 'string' && value.length >= 6) resolvedValues.add(value);
12
- return value;
9
+ const remember = (value) => {
10
+ if (typeof value === 'string' && value.length >= 6)
11
+ resolvedValues.add(value);
12
+ return value;
13
13
  };
14
-
15
14
  /** Filter credentials resolved by this process; the values never leave their resolution owner. */
16
15
  export function redactResolvedSecrets(text) {
17
- let clean = text;
18
- for (const value of resolvedValues) if (clean.includes(value)) clean = clean.split(value).join('[redacted:resolved-secret]');
19
- return clean;
16
+ let clean = text;
17
+ for (const value of resolvedValues)
18
+ if (clean.includes(value))
19
+ clean = clean.split(value).join('[redacted:resolved-secret]');
20
+ return clean;
20
21
  }
21
-
22
22
  /** Check lexical availability only; blank and conventional template markers never prove provider validity. */
23
- export function credentialPresent(value){
24
- if(typeof value!=='string'||!value.trim())return false;
25
- return !/^(?:CHANGE_ME|REPLACE_ME|PLACEHOLDER|TODO|<[^>]*>|YOUR_[A-Z0-9_]+)$/i.test(value.trim());
23
+ export function credentialPresent(value) {
24
+ if (typeof value !== 'string' || !value.trim())
25
+ return false;
26
+ return !/^(?:CHANGE_ME|REPLACE_ME|PLACEHOLDER|TODO|<[^>]*>|YOUR_[A-Z0-9_]+)$/i.test(value.trim());
26
27
  }
27
-
28
28
  /** Read one configured credential file with a finite byte budget; no effects beyond the read. */
29
- export function readSecretBytes(file){
30
- const before=fs.lstatSync(file);
31
- if(!before.isFile()||before.isSymbolicLink())throw new Error('credential file must be a regular file');
32
- if(before.size>CREDENTIAL_FILE_MAX_BYTES)throw new Error('credential file exceeds the byte budget');
33
- const fd=fs.openSync(file,fs.constants.O_RDONLY|(fs.constants.O_NOFOLLOW??0));
34
- let bytes;
35
- try{
36
- const stat=fs.fstatSync(fd);
37
- if(!stat.isFile()||stat.dev!==before.dev||stat.ino!==before.ino)throw new Error('credential file changed during resolution');
38
- bytes=Buffer.alloc(CREDENTIAL_FILE_MAX_BYTES+1);
39
- let count=0;
40
- while(count<bytes.length){const n=fs.readSync(fd,bytes,count,bytes.length-count,null);if(n===0){break;}count+=n;}
41
- if(count>CREDENTIAL_FILE_MAX_BYTES)throw new Error('credential file exceeds the byte budget');
42
- return bytes.subarray(0,count);
43
- }catch(error){bytes?.fill(0);throw error;}finally{fs.closeSync(fd);}
29
+ export function readSecretBytes(file) {
30
+ const before = fs.lstatSync(file);
31
+ if (!before.isFile() || before.isSymbolicLink())
32
+ throw new Error('credential file must be a regular file');
33
+ if (before.size > CREDENTIAL_FILE_MAX_BYTES)
34
+ throw new Error('credential file exceeds the byte budget');
35
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
36
+ let bytes;
37
+ try {
38
+ const stat = fs.fstatSync(fd);
39
+ if (!stat.isFile() || stat.dev !== before.dev || stat.ino !== before.ino)
40
+ throw new Error('credential file changed during resolution');
41
+ bytes = Buffer.alloc(CREDENTIAL_FILE_MAX_BYTES + 1);
42
+ let count = 0;
43
+ while (count < bytes.length) {
44
+ const n = fs.readSync(fd, bytes, count, bytes.length - count, null);
45
+ if (n === 0) {
46
+ break;
47
+ }
48
+ count += n;
49
+ }
50
+ if (count > CREDENTIAL_FILE_MAX_BYTES)
51
+ throw new Error('credential file exceeds the byte budget');
52
+ return bytes.subarray(0, count);
53
+ }
54
+ catch (error) {
55
+ bytes?.fill(0);
56
+ throw error;
57
+ }
58
+ finally {
59
+ fs.closeSync(fd);
60
+ }
44
61
  }
45
-
46
- const readCredentialFile = file => { const bytes=readSecretBytes(file); try{return bytes.toString('utf8');}finally{bytes.fill(0);} };
47
-
48
- const DOTENV_LINE_PARTS = [String.raw`^\s*`, String.raw`(?:export\s+)?`, String.raw`([A-Za-z_]\w*)`, String.raw`\s*=\s*`, String.raw`([^\r\n\u2028\u2029]*)`, String.raw`\s*$`];
62
+ const readCredentialFile = (file) => { const bytes = readSecretBytes(file); try {
63
+ return bytes.toString('utf8');
64
+ }
65
+ finally {
66
+ bytes.fill(0);
67
+ } };
68
+ const DOTENV_LINE_PARTS = [String.raw `^\s*`, String.raw `(?:export\s+)?`, String.raw `([A-Za-z_]\w*)`, String.raw `\s*=\s*`, String.raw `([^\r\n\u2028\u2029]*)`, String.raw `\s*$`];
49
69
  const DOTENV_LINE = new RegExp(DOTENV_LINE_PARTS.join(''));
50
-
51
70
  /** Parse a dotenv file (KEY=VALUE lines, # comments, optional export/quotes). An absent file is {}. */
52
- export function readDotenv(file){
53
- let text='';try{text=readCredentialFile(file);}catch(error){if(error?.code==='ENOENT'){return {};}throw error;}
54
- const out={};
55
- for(const line of text.split(/\r?\n/)){
56
- const m=DOTENV_LINE.exec(line);
57
- if(!m)continue;
58
- let value=m[2].trimEnd();
59
- if(value.length>=2&&(value.startsWith('"')||value.startsWith("'"))&&value.at(-1)===value[0])value=value.slice(1,-1);
60
- out[m[1]]=value;
61
- }
62
- return out;
71
+ export function readDotenv(file) {
72
+ let text = '';
73
+ try {
74
+ text = readCredentialFile(file);
75
+ }
76
+ catch (error) {
77
+ if (error?.code === 'ENOENT') {
78
+ return {};
79
+ }
80
+ throw error;
81
+ }
82
+ const out = {};
83
+ for (const line of text.split(/\r?\n/)) {
84
+ const m = DOTENV_LINE.exec(line);
85
+ if (!m)
86
+ continue;
87
+ const name = m[1];
88
+ let value = m[2].trimEnd();
89
+ if (value.length >= 2 && (value.startsWith('"') || value.startsWith("'")) && value.at(-1) === value[0])
90
+ value = value.slice(1, -1);
91
+ out[name] = value;
92
+ }
93
+ return out;
63
94
  }
64
-
65
95
  /**
66
96
  * Resolve a configured variable or its supported NAME_FILE custody pointer. An explicitly supplied
67
97
  * blank variable disables the value; it must not resurrect a file value. Never print the result.
68
98
  */
69
- export function connectorSecret(name,env=process.env){
70
- if(typeof name!=='string'||!ENV_NAME.test(name))return null;
71
- if(Object.hasOwn(env,name))return typeof env[name]==='string'?remember(env[name].trim()||null):null;
72
- const pointer=typeof env[`${name}_FILE`]==='string'?env[`${name}_FILE`].trim():'';
73
- if(!pointer)return null;
74
- try{const value=readCredentialFile(pointer).trim();return remember(value||null);}catch{return null;}
99
+ export function connectorSecret(name, env = process.env) {
100
+ if (typeof name !== 'string' || !ENV_NAME.test(name))
101
+ return null;
102
+ const direct = env[name];
103
+ if (Object.hasOwn(env, name))
104
+ return typeof direct === 'string' ? remember(direct.trim() || null) : null;
105
+ const filePointer = env[`${name}_FILE`];
106
+ const pointer = typeof filePointer === 'string' ? filePointer.trim() : '';
107
+ if (!pointer)
108
+ return null;
109
+ try {
110
+ const value = readCredentialFile(pointer).trim();
111
+ return remember(value || null);
112
+ }
113
+ catch {
114
+ return null;
115
+ }
75
116
  }
76
-
77
117
  /**
78
118
  * Load the verified runtime main's local credentials below the actual environment without mutating
79
119
  * either input. The caller owns repository identity; this base-tier reader never infers a root from cwd.
80
120
  * Absence is allowed so the action-specific gate can name only the credentials that action requires.
81
121
  */
82
- export function secretEnv(verifiedRuntimeRoot,env=process.env){
83
- if(typeof verifiedRuntimeRoot!=='string'||!path.isAbsolute(verifiedRuntimeRoot))
84
- throw new TypeError('secretEnv requires an absolute verified runtime root');
85
- if(env===null||typeof env!=='object'||Array.isArray(env))throw new TypeError('secretEnv requires an environment mapping');
86
- const root=fs.lstatSync(verifiedRuntimeRoot);
87
- if(!root.isDirectory()||root.isSymbolicLink())throw new Error('secretEnv runtime root must be a regular directory');
88
- const file=path.join(verifiedRuntimeRoot,SECRET_ENV_FILE);
89
- let stat;
90
- try{stat=fs.lstatSync(file);}catch(error){if(error?.code==='ENOENT'){return {...env};}throw error;}
91
- if(!stat.isFile()||stat.isSymbolicLink())throw new Error('secret.env must be a regular file');
92
- const local = readDotenv(file);
93
- const resolved = {...local,...env};
94
- for (const name of Object.keys(local)) if (typeof resolved[name] === 'string') remember(resolved[name]);
95
- return resolved;
122
+ export function secretEnv(verifiedRuntimeRoot, env = process.env) {
123
+ if (typeof verifiedRuntimeRoot !== 'string' || !path.isAbsolute(verifiedRuntimeRoot))
124
+ throw new TypeError('secretEnv requires an absolute verified runtime root');
125
+ if (env === null || typeof env !== 'object' || Array.isArray(env))
126
+ throw new TypeError('secretEnv requires an environment mapping');
127
+ const root = fs.lstatSync(verifiedRuntimeRoot);
128
+ if (!root.isDirectory() || root.isSymbolicLink())
129
+ throw new Error('secretEnv runtime root must be a regular directory');
130
+ const file = path.join(verifiedRuntimeRoot, SECRET_ENV_FILE);
131
+ let stat;
132
+ try {
133
+ stat = fs.lstatSync(file);
134
+ }
135
+ catch (error) {
136
+ if (error?.code === 'ENOENT') {
137
+ return { ...env };
138
+ }
139
+ throw error;
140
+ }
141
+ if (!stat.isFile() || stat.isSymbolicLink())
142
+ throw new Error('secret.env must be a regular file');
143
+ const local = readDotenv(file);
144
+ const resolved = { ...local, ...env };
145
+ for (const name of Object.keys(local))
146
+ if (typeof resolved[name] === 'string')
147
+ remember(resolved[name]);
148
+ return resolved;
96
149
  }
97
-
98
- const sopsRefuse=(reason,message)=>{
99
- const error=new Error(`SOPS identity [${reason}]: ${message}`);
100
- error.name='SopsIdentityRefusal';error.identityRefusal=reason;
101
- return {mode:'refused',env:null,error};
150
+ const sopsRefuse = (reason, message) => {
151
+ const error = new Error(`SOPS identity [${reason}]: ${message}`);
152
+ error.name = 'SopsIdentityRefusal';
153
+ error.identityRefusal = reason;
154
+ return { mode: 'refused', env: null, error };
102
155
  };
103
- const sopsInlineIdentity=(env,name)=>{
104
- if(!credentialPresent(env[name]))return sopsRefuse('disabled-inline','SOPS_AGE_KEY is explicitly empty or invalid');
105
- remember(env[name].trim());
106
- return {...sopsRefuse('inline-context-unqualified','the supplied SOPS_AGE_KEY awaits an owned selected-identity isolation capability; no SOPS process was launched'),inlineName:name};
156
+ const sopsInlineIdentity = (env, name) => {
157
+ const inline = env[name];
158
+ if (!credentialPresent(inline))
159
+ return sopsRefuse('disabled-inline', 'SOPS_AGE_KEY is explicitly empty or invalid');
160
+ remember(inline.trim());
161
+ return { ...sopsRefuse('inline-context-unqualified', 'the supplied SOPS_AGE_KEY awaits an owned selected-identity isolation capability; no SOPS process was launched'), inlineName: name };
107
162
  };
108
- const sopsFileIdentity=(env,file)=>{
109
- if(!credentialPresent(file))return sopsRefuse('invalid-file','the original identity file selection is empty or invalid');
110
- if(!path.isAbsolute(file))return sopsRefuse('identity-file-location-unqualified','select the original identity by its absolute path; caller and SOPS working directories may differ');
111
- try{
112
- const stat=fs.lstatSync(file);
113
- if(!stat.isFile()||stat.isSymbolicLink())return sopsRefuse('identity-file-unavailable','the explicitly selected original identity must be a regular non-symlink file');
114
- }catch{return sopsRefuse('identity-file-unavailable','the explicitly selected original identity file is unavailable');}
115
- return {mode:'file',env:{...env,SOPS_AGE_KEY_FILE:file},error:null};
163
+ const sopsFileIdentity = (env, file) => {
164
+ if (!credentialPresent(file))
165
+ return sopsRefuse('invalid-file', 'the original identity file selection is empty or invalid');
166
+ if (!path.isAbsolute(file))
167
+ return sopsRefuse('identity-file-location-unqualified', 'select the original identity by its absolute path; caller and SOPS working directories may differ');
168
+ try {
169
+ const stat = fs.lstatSync(file);
170
+ if (!stat.isFile() || stat.isSymbolicLink())
171
+ return sopsRefuse('identity-file-unavailable', 'the explicitly selected original identity must be a regular non-symlink file');
172
+ }
173
+ catch {
174
+ return sopsRefuse('identity-file-unavailable', 'the explicitly selected original identity file is unavailable');
175
+ }
176
+ return { mode: 'file', env: { ...env, SOPS_AGE_KEY_FILE: file }, error: null };
116
177
  };
117
178
  /** Select a supplied age identity without inventing custody or running a key tool. Inline execution awaits its owned isolation capability. */
118
- export function sopsIdentityEnv(env,{identity=null,required=false,platform=process.platform}={}){
119
- if(!env||typeof env!=='object'||Array.isArray(env))return sopsRefuse('invalid-environment','an environment mapping is required');
120
- const names=name=>Object.keys(env).filter(key=>platform==='win32'?key.toUpperCase()===name:key===name);
121
- const inline=names('SOPS_AGE_KEY'),files=names('SOPS_AGE_KEY_FILE');
122
- if(inline.length>1||files.length>1)return sopsRefuse('ambiguous-environment','multiple Windows aliases select an age identity');
123
- if(inline.length)return sopsInlineIdentity(env,inline[0]);
124
- let file=identity;
125
- if(files.length){
126
- const value=env[files[0]];
127
- if(!credentialPresent(value))return sopsRefuse('disabled-file','SOPS_AGE_KEY_FILE is explicitly empty or invalid');
128
- if(identity!==null&&identity!==undefined&&identity!==value)return sopsRefuse('conflicting-files','two different original identity files were explicitly selected');
129
- file=value;
130
- }
131
- if(file!==null&&file!==undefined)return sopsFileIdentity(env,file);
132
- if(required)return sopsRefuse('identity-not-supplied','supply SOPS_AGE_KEY locally or select the original SOPS_AGE_KEY_FILE; no home default was inferred');
133
- return {mode:'public-recipient',env:{...env},error:null};
179
+ export function sopsIdentityEnv(env, { identity = null, required = false, platform = process.platform } = {}) {
180
+ if (!env || typeof env !== 'object' || Array.isArray(env))
181
+ return sopsRefuse('invalid-environment', 'an environment mapping is required');
182
+ const names = (name) => Object.keys(env).filter(key => platform === 'win32' ? key.toUpperCase() === name : key === name);
183
+ const inline = names('SOPS_AGE_KEY'), files = names('SOPS_AGE_KEY_FILE');
184
+ if (inline.length > 1 || files.length > 1)
185
+ return sopsRefuse('ambiguous-environment', 'multiple Windows aliases select an age identity');
186
+ if (inline.length)
187
+ return sopsInlineIdentity(env, inline[0]);
188
+ let file = identity;
189
+ if (files.length) {
190
+ const value = env[files[0]];
191
+ if (!credentialPresent(value))
192
+ return sopsRefuse('disabled-file', 'SOPS_AGE_KEY_FILE is explicitly empty or invalid');
193
+ if (identity !== null && identity !== undefined && identity !== value)
194
+ return sopsRefuse('conflicting-files', 'two different original identity files were explicitly selected');
195
+ file = value;
196
+ }
197
+ if (file !== null && file !== undefined)
198
+ return sopsFileIdentity(env, file);
199
+ if (required)
200
+ return sopsRefuse('identity-not-supplied', 'supply SOPS_AGE_KEY locally or select the original SOPS_AGE_KEY_FILE; no home default was inferred');
201
+ return { mode: 'public-recipient', env: { ...env }, error: null };
134
202
  }
@@ -1,21 +1,24 @@
1
- import {isPlainObject as plain} from './plain-object.mjs';
2
- import {invalid} from './invalid-config.mjs';
3
-
1
+ import { isPlainObject as plain } from './plain-object.mjs';
2
+ import { invalid } from './invalid-config.mjs';
4
3
  /** The keys of the sonar block. */
5
- const SONAR_KEYS=Object.freeze(['organization']);
4
+ const SONAR_KEYS = Object.freeze(['organization']);
6
5
  /** A SonarCloud organization key: lowercase letters, digits, hyphens and underscores, starting with a letter or a digit. */
7
- const ORGANIZATION=/^[a-z0-9][a-z0-9_-]*$/;
8
-
6
+ const ORGANIZATION = /^[a-z0-9][a-z0-9_-]*$/;
9
7
  /**
10
8
  * config.yaml `sonar` - {organization?}: the key of the SonarCloud organization the release's Sonar proof of the example apps analyses in
11
9
  * (the project key is <organization>_<key the example declares>). It is configuration, not a secret; the environment variable
12
10
  * SONAR_ORGANIZATION wins over it, the repository variable of the same name is what CI reads. Null or absent means "not set yet".
13
11
  */
14
- export function validateSonar(sonar){
15
- if(sonar===null)return;
16
- const bad=invalid('sonar');
17
- if(!plain(sonar))bad(' must be {organization?: <SonarCloud organization key>} or null.');
18
- for(const key of Object.keys(sonar))if(!SONAR_KEYS.includes(key))bad(` has unknown key ${key} (allowed: ${SONAR_KEYS.join(', ')}).`);
19
- const value=sonar.organization;
20
- if(value!==undefined&&value!==null&&!(typeof value==='string'&&ORGANIZATION.test(value)))bad('.organization must be a SonarCloud organization key (lowercase letters, digits, - and _) or null.');
12
+ export function validateSonar(sonar) {
13
+ if (sonar === null)
14
+ return;
15
+ const bad = invalid('sonar');
16
+ if (!plain(sonar))
17
+ return bad(' must be {organization?: <SonarCloud organization key>} or null.');
18
+ for (const key of Object.keys(sonar))
19
+ if (!SONAR_KEYS.includes(key))
20
+ bad(` has unknown key ${key} (allowed: ${SONAR_KEYS.join(', ')}).`);
21
+ const value = sonar.organization;
22
+ if (value !== undefined && value !== null && !(typeof value === 'string' && ORGANIZATION.test(value)))
23
+ bad('.organization must be a SonarCloud organization key (lowercase letters, digits, - and _) or null.');
21
24
  }
@@ -17,7 +17,7 @@ pins:
17
17
  # --- @starci packages. Every @starci package is a PUBLISHED npm package (install: registry): a product repository
18
18
  # installs the exact pinned version from the npm registry in the root and every workspace package.json, never `file:`.
19
19
  '@starci/heroicons':
20
- version: 0.3.4
20
+ version: 0.3.5
21
21
  group: starci
22
22
  install: registry
23
23
  side: fe
@@ -31,35 +31,35 @@ pins:
31
31
  source: packages/event-stream/package.json
32
32
  why: 'the one server-sent-events client of every front end (fe-stack event-stream, owner:event-stream-package): the transport client layer (modules/api/sse/clients) creates one instance for the app with the bearer token source and the stream operations follow on it; it carries Last-Event-ID resume, the capped doubling backoff, the heartbeat timeout, the single 401 renewal and the useSWRSubscription adapter, so no app keeps a hand-written reader.'
33
33
  '@starci/grammar':
34
- version: 0.15.34
34
+ version: 0.16.2
35
35
  group: starci
36
36
  install: registry
37
37
  side: fe
38
38
  source: packages/grammar/package.json
39
39
  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.'
40
40
  '@starci/eslint-canon-be':
41
- version: 3.8.21
41
+ version: 3.9.2
42
42
  group: starci
43
43
  install: registry
44
44
  side: be
45
45
  source: packages/eslint-canon-be/package.json
46
46
  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.'
47
47
  '@starci/eslint-canon-fe':
48
- version: 9.5.23
48
+ version: 9.6.2
49
49
  group: starci
50
50
  install: registry
51
51
  side: fe
52
52
  source: packages/eslint-canon-fe/package.json
53
53
  why: 'the front-end ESLint canon: `loadHfs(import.meta.url)` of fe/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the fe 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; the front end has no tests (FE_NO_TESTS R97 in hfs) and no-vietnamese-in-source (R91) applies.'
54
54
  '@starci/eslint-grammar-core':
55
- version: 0.15.34
55
+ version: 0.16.2
56
56
  group: starci
57
57
  install: registry
58
58
  side: fe
59
59
  source: packages/eslint-grammar-core/package.json
60
60
  why: 'the StarCi Grammar component API lint, versioned with @starci/grammar; front ends compose this family package with the universal FE canon.'
61
61
  '@starci/eslint-grammar-offset-pop':
62
- version: 0.15.34
62
+ version: 0.16.2
63
63
  group: starci
64
64
  install: registry
65
65
  side: fe
@@ -85,28 +85,28 @@ pins:
85
85
  side: both
86
86
  source: packages/prettier-config/package.json
87
87
  '@starci/jest-preset':
88
- version: 2.5.2
88
+ version: 2.5.3
89
89
  group: starci
90
90
  install: registry
91
91
  side: be
92
92
  source: packages/jest-preset/package.json
93
93
  why: 'the Jest preset of a back end: the world runner runs up to min(--maxWorkers, slots) world files at once, each in its own process bound to one data slot of the test world (STARCI_TEST_WORLD_SLOT), and reads test-world state protocol 2 only: it pairs exactly with @starci/test-world, and any other state file is JEST_PRESET_WORLD_PAIR_MISMATCH. The unit run writes the lcov Sonar and Codecov import (services only); the integration, e2e and contract projects run every spec file in a worker process of its own (world-runner.cjs), so a process-global framework registry (@nestjs/graphql type metadata) never leaks between e2e files; it ships the unit kit (mockEntityManager, fakeTransaction, fakeQueryBuilder, queueQueryBuilders, fakeIds, FakeClock, Outcome matchers) and the recordingOutbox claim side.'
94
94
  '@starci/test-world':
95
- version: 1.3.2
95
+ version: 1.3.3
96
96
  group: starci
97
97
  install: registry
98
98
  side: be
99
99
  source: packages/test-world/package.json
100
100
  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. A fake declared https (defineHttpFake) serves TLS with the run''s ephemeral in-memory fake CA and publishes its PEM as w.fake.<name>.ca for the app''s own client to trust; verification is never turned off. 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.'
101
101
  '@starci/ai-router':
102
- version: 0.4.0
102
+ version: 0.4.1
103
103
  group: starci
104
104
  install: registry
105
105
  side: be
106
106
  source: packages/ai-router/package.json
107
107
  why: 'the AI pool router of a back end: a NestJS module that routes OpenAI-compatible chat, stream and embeddings calls across keys, models and providers, with @starci/ai-router/testing for its specs; @starci/jest-preset compiles it as an ESM-only dependency.'
108
108
  '@starci/hfs':
109
- version: 4.11.22
109
+ version: 4.12.2
110
110
  group: starci
111
111
  install: registry
112
112
  side: both
@@ -19,7 +19,7 @@
19
19
  schema: starci/hfs-rules@3
20
20
  version: 3.1.0
21
21
  gates:
22
- pre-commit: the managed L0 hook runs app hygiene, managed-file drift, ESLint per side and stylelint on staged source, and prettier on staged files; typecheck and targeted specs belong to the op gate under R225
22
+ pre-commit: the managed L0 hook runs app check --staged, managed-file drift, ESLint per side and stylelint on staged source, and prettier on staged files; typecheck and targeted specs belong to the op gate under R225
23
23
  pre-push: the managed app hook invokes the bundled push policy under R225; it starts no source check, build or test
24
24
  settle: the op gate `starci gate run` over the op's changed files (merge guard, starci app lint --changed, codegen, tsc, the slice's specs; only findings new against the workflow's previous checkpoint block), re-read at starci kernel settle, which refuses a red done; a green settle commits the op's checkpoint on wf-<workflowId>
25
25
  land: the workflow's finish, the only time main is touched - gate.mjs over the whole workflow branch against its merge-base with main, the merge guard, review.verify of the exact head that lands, the branch rebased onto main, then main fast-forwarded and pushed
@@ -918,7 +918,7 @@ rules:
918
918
  code: "BE_TEST_TOPOLOGY"
919
919
  editions: [lite, full]
920
920
  docs: [docs/quality-bar.md#r47-test-topology]
921
- law: "One jest config, projects `unit`, `integration`, `e2e` and `contract`, live by folder, `diagnostics: false`; unit specs are `<name>.<role>.spec.ts` beside the file they test: required for the roles of `ruleParams.be.unitRoles` (`<name>.service.spec.ts` beside a `<name>.service.ts`, `<name>.cli.spec.ts` beside a cli command `<name>.cli.ts` of `src/features/cli`), optional for any other logic role (`ruleParams.be.logicRoles`) inside a slot whose `coverage` is required and beside a logic file such a slot declares (`logic`: `passport.tokens.spec.ts` beside `passport.tokens.ts`), except that the commit adding a measured logic file stages its `<name>.<role>.spec.ts` beside it (owner 2026-10-20, judged by `starci app hygiene` in the pre-commit hook), and nowhere else but beside a webhook or realtime door that has no service of its own (`<provider>.webhook.spec.ts`, `<channel>.gateway.spec.ts`, `<x>.subscription.spec.ts`) (every unit-tested subject has exactly one; any other `*.spec.ts` outside `be/src/tests/{world,integration,e2e,contract}` is a finding, including specs in apps), and the unit project collects coverage from the logic roles of the measured roots and the logic files their slots declare only (the scope derived from the slot manifest, R204) with a per-file threshold of 100 on lines, branches, functions and statements (`test` runs `--coverage`; `importHelpers` and `tslib`); `be/src/tests/world/` is the ONLY test infrastructure: `global-setup.ts` starts the app's own stack (`.starcistacks/<env>`, named by `stack` in `test-world.config.ts`, the declaration of the test-world library in its one named form `export const { useTestWorld, useSandbox } = defineTestWorld({...})` (a default export is not read): every service it declares runs REAL, behind toxiproxy, images and versions read from the stack at run time, never spelled in test source) or attaches to a warm one that answers, and runs the migrations once over the cli app's connections (the runner of the cli migrate command); `global-teardown.ts` stops only what its setup started; `use-test-world.ts` exports `useTestWorld({ apps } | { modules })` (`world.apps.<name>.api`, `world.db.<connection>`, `world.infra.<service>` with `latency(ms)`, `cut()` and `restore()` on the real service, `world.fake.<provider>`, `world.waitFor`) and `fakes/<provider>/` holds the network-edge fakes (`failNext`, `replayWebhook`, `delay`, payload fixtures) of external SaaS the team does not operate ONLY: a fake of a service the stack declares is refused, except stateless compute that needs special hardware or an external model (GPU inference, a self-hosted embedding model), whose `stacks` entry in `test-world.config.ts` declares `{ fakedBy: <fake>, reason }` with a non-empty `reason` (a stateful service, or one with a persistent volume, is never accepted); it alone owns containers, DataSource, migrations and `process.env`; nothing under `be/src/tests` overrides a provider or DI token; `be/src/tests/integration/<capability>/*.integration-spec.ts` (`{ modules }`, with the real peer apps its client calls beside them as `apps`; real infrastructure, no HTTP door of ours), `be/src/tests/e2e/<area>/*.e2e-spec.ts` (`{ apps }`) and `be/src/tests/contract/<provider>/*.contract-spec.ts` (provider sandboxes, skipped without sandbox config, run only by `test:contract`, never by `test` or `test:e2e`) agree folder with suffix; an integration or e2e spec passes `useTestWorld` one object literal with plain keys (no variable, spread or computed key) and calls it; the contract layer has no skip either: a contract spec writes `sandbox.describe(...)` from the test-world library's `useSandbox(...)`, and when the sandbox config is absent that describe registers one failing test naming the missing keys (the body never runs), and every `fakes/<provider>/` that serves payload fixtures (`payloads/*.json`) has a `contract/<provider>/*.contract-spec.ts` that references those fixtures and asserts `shapeOf(real)` equals `shapeOf(fixture)` (the helper `shapeOf` is named by `ruleParams.be.contractShape` of the slot manifest; fakes the test-world library provides are guarded by the library); no `.test.ts`, `int-spec` or `harness-spec`, no `live/` and no `be/src/tests/e2e/world`."
921
+ law: "One jest config, projects `unit`, `integration`, `e2e` and `contract`, live by folder, `diagnostics: false`; unit specs are `<name>.<role>.spec.ts` beside the file they test: required for the roles of `ruleParams.be.unitRoles` (`<name>.service.spec.ts` beside a `<name>.service.ts`, `<name>.cli.spec.ts` beside a cli command `<name>.cli.ts` of `src/features/cli`), optional for any other logic role (`ruleParams.be.logicRoles`) inside a slot whose `coverage` is required and beside a logic file such a slot declares (`logic`: `passport.tokens.spec.ts` beside `passport.tokens.ts`), except that the commit adding a measured logic file stages its `<name>.<role>.spec.ts` beside it (owner 2026-10-20, judged by `starci app check --staged` in the pre-commit hook), and nowhere else but beside a webhook or realtime door that has no service of its own (`<provider>.webhook.spec.ts`, `<channel>.gateway.spec.ts`, `<x>.subscription.spec.ts`) (every unit-tested subject has exactly one; any other `*.spec.ts` outside `be/src/tests/{world,integration,e2e,contract}` is a finding, including specs in apps), and the unit project collects coverage from the logic roles of the measured roots and the logic files their slots declare only (the scope derived from the slot manifest, R204) with a per-file threshold of 100 on lines, branches, functions and statements (`test` runs `--coverage`; `importHelpers` and `tslib`); `be/src/tests/world/` is the ONLY test infrastructure: `global-setup.ts` starts the app's own stack (`.starcistacks/<env>`, named by `stack` in `test-world.config.ts`, the declaration of the test-world library in its one named form `export const { useTestWorld, useSandbox } = defineTestWorld({...})` (a default export is not read): every service it declares runs REAL, behind toxiproxy, images and versions read from the stack at run time, never spelled in test source) or attaches to a warm one that answers, and runs the migrations once over the cli app's connections (the runner of the cli migrate command); `global-teardown.ts` stops only what its setup started; `use-test-world.ts` exports `useTestWorld({ apps } | { modules })` (`world.apps.<name>.api`, `world.db.<connection>`, `world.infra.<service>` with `latency(ms)`, `cut()` and `restore()` on the real service, `world.fake.<provider>`, `world.waitFor`) and `fakes/<provider>/` holds the network-edge fakes (`failNext`, `replayWebhook`, `delay`, payload fixtures) of external SaaS the team does not operate ONLY: a fake of a service the stack declares is refused, except stateless compute that needs special hardware or an external model (GPU inference, a self-hosted embedding model), whose `stacks` entry in `test-world.config.ts` declares `{ fakedBy: <fake>, reason }` with a non-empty `reason` (a stateful service, or one with a persistent volume, is never accepted); it alone owns containers, DataSource, migrations and `process.env`; nothing under `be/src/tests` overrides a provider or DI token; `be/src/tests/integration/<capability>/*.integration-spec.ts` (`{ modules }`, with the real peer apps its client calls beside them as `apps`; real infrastructure, no HTTP door of ours), `be/src/tests/e2e/<area>/*.e2e-spec.ts` (`{ apps }`) and `be/src/tests/contract/<provider>/*.contract-spec.ts` (provider sandboxes, skipped without sandbox config, run only by `test:contract`, never by `test` or `test:e2e`) agree folder with suffix; an integration or e2e spec passes `useTestWorld` one object literal with plain keys (no variable, spread or computed key) and calls it; the contract layer has no skip either: a contract spec writes `sandbox.describe(...)` from the test-world library's `useSandbox(...)`, and when the sandbox config is absent that describe registers one failing test naming the missing keys (the body never runs), and every `fakes/<provider>/` that serves payload fixtures (`payloads/*.json`) has a `contract/<provider>/*.contract-spec.ts` that references those fixtures and asserts `shapeOf(real)` equals `shapeOf(fixture)` (the helper `shapeOf` is named by `ruleParams.be.contractShape` of the slot manifest; fakes the test-world library provides are guarded by the library); no `.test.ts`, `int-spec` or `harness-spec`, no `live/` and no `be/src/tests/e2e/world`."
922
922
  kinds: [check, codemod, lint]
923
923
  gates: [land, ci]
924
924
  failureCodes: ["BE_TEST_TOPOLOGY", "BE_CONTRACT_UNGUARDED", "BE_UNIT_SPEC_NOT_STAGED"]
@@ -3619,7 +3619,7 @@ rules:
3619
3619
  docs: [docs/lite-edition.md#edition-validation]
3620
3620
  templateNa: "The hfs.json edition declaration is generated programmatically by packages/hfs/scaffold/app.mjs; no managed declaration template exists. Forbidden paths are rejected rather than scaffolded."
3621
3621
  code: "HFS_EDITION_INVALID"
3622
- law: "hfs.json names an edition the manifest knows (full, or lite; absent means full). Lite is a minimal production (case-10): the same monorepo, code structure, slots, patterns and back-end unit specs, one stateless instance type that scales horizontally, its own PostgreSQL db service, Passport identity and S3-compatible storage (ruling:lite-self-hosted-postgres-passport-minio), and one minimal instance of each backing service a declared capability needs (hfs.json sides.be.backing: Redis for the queue, with AOF persistence); vector search uses pgvector in the db service (owner:lite-vector-is-pgvector, which excludes Qdrant from lite); a backing service is declared or absent, never on by default. The queue is BullMQ on Redis in both editions, fed by the Postgres outbox; pg_cron ticks enqueue through that outbox; the event bus keeps its lite adapter (pgmq-backed topics or in-process delivery) because full's event bus is Kafka, not Redis; cache and locks are Postgres unless Redis is declared; the api and sidecars run on docker-swarm on a VPS. The realtime pattern serves SSE (the default for one-way push), the same socket.io gateway or the restricted GraphQL subscription inside the be api process (scales horizontally and stateless: the memory hub is for a single replica or tests only, and replicas > 1 use the Redis hub and the sharded @socket.io/redis-adapter when Redis is declared, else the Postgres hub and @socket.io/postgres-adapter; both shared socket adapters force websocket-only connections with no sticky sessions; R212 case-9, which amends the realtime clauses of case-2, case-3 and case-5; the GraphQL subscription door is R212 case-7 and owner:lite-graphql-subscription-door), and both editions share knowledge/hfs/fe-stack.yaml. Lite requires application-stacks.yaml and permits environment metadata, runbooks, infra/swarm and encrypted-secret custody, while refusing infra/compose, infra/k8s, infra/terraform and backing components other than its db service (runtime/{config,env}, seeds and the sealed secrets of its environments are allowed). Contract specs and `test:contract` are allowed (owner 2026-10-14); it has no e2e spec, `test:e2e`, `test:stack` or `test:browser` script, e2e workflow, browser journey, Work catalog, shell, features, resources, UAT or br records, test container, toxiproxy, browser-runner or Kafka dependency, client library of an undeclared backing service (HFS_BACKING_UNDECLARED), declared backing service without a consumer (HFS_BACKING_UNUSED), or a supabase/ tree (LITE_DB_MIGRATION). Its .starciwork is optional and admits only .gitignore, workspace.yaml and brand/**; workspace.yaml is optional and carries project identity and repo roles for the Work tools."
3622
+ law: "hfs.json names an edition the manifest knows (full, or lite; absent means full). Lite is a minimal production (case-10): the same monorepo, code structure, slots, patterns and back-end unit specs, one stateless instance type that scales horizontally, its own PostgreSQL db service, Passport identity and S3-compatible storage (ruling:lite-self-hosted-postgres-passport-minio), and one minimal instance of each backing service a declared capability needs (hfs.json sides.be.backing: Redis for the queue, with AOF persistence); vector search uses pgvector in the db service (owner:lite-vector-is-pgvector, which excludes Qdrant from lite); a backing service is declared or absent, never on by default. The queue is BullMQ on Redis in both editions, fed by the Postgres outbox; pg_cron ticks enqueue through that outbox; the event bus keeps its lite adapter (pgmq-backed topics or in-process delivery) because full's event bus is Kafka, not Redis; cache and locks are Postgres unless Redis is declared; the api and sidecars run on docker-swarm on a VPS. The realtime pattern serves SSE (the default for one-way push), the same socket.io gateway or the restricted GraphQL subscription inside the be api process (scales horizontally and stateless: the memory hub is for a single replica or tests only, and replicas > 1 use the Redis hub and the sharded @socket.io/redis-adapter when Redis is declared, else the Postgres hub and @socket.io/postgres-adapter; both shared socket adapters force websocket-only connections with no sticky sessions; R212 case-9, which amends the realtime clauses of case-2, case-3 and case-5; the GraphQL subscription door is R212 case-7 and owner:lite-graphql-subscription-door), and both editions share knowledge/hfs/fe-stack.yaml. Lite requires application-stacks.yaml and permits environment metadata, runbooks, infra/swarm and encrypted-secret custody, while refusing infra/compose, infra/k8s, infra/terraform and backing components other than its db service (runtime/{config,env}, seeds and the sealed secrets of its environments are allowed). Contract specs and `test:contract` are allowed (owner 2026-10-14); it has no e2e spec, `test:e2e`, `test:world` or `test:browser` script, e2e workflow, browser journey, Work catalog, shell, features, resources, UAT or br records, test container, toxiproxy, browser-runner or Kafka dependency, client library of an undeclared backing service (HFS_BACKING_UNDECLARED), declared backing service without a consumer (HFS_BACKING_UNUSED), or a supabase/ tree (LITE_DB_MIGRATION). Its .starciwork is optional and admits only .gitignore, workspace.yaml and brand/**; workspace.yaml is optional and carries project identity and repo roles for the Work tools."
3623
3623
  kinds: [check]
3624
3624
  gates: [settle, land, ci]
3625
3625
  failureCodes: ["HFS_EDITION_INVALID", "HFS_EDITION_FORBIDDEN_PRESENT", "HFS_STARCIWORK_LITE_FORBIDDEN", "HFS_BACKING_UNDECLARED", "HFS_BACKING_UNUSED"]
@@ -4157,7 +4157,7 @@ rules:
4157
4157
  code: "ENV_VALUE_SHAPE"
4158
4158
  editions: [lite, full]
4159
4159
  docs: [docs/host-secrets.md#r249]
4160
- law: "A stored environment value parses as the type its `EnvSource` reader declares (an integer, `true`/`false`, an absolute URL, a duration `250`/`30s`/`5m`/`2h`, a member of the declared enum, a nonempty string or secret): `starci app secret verify <env>` decrypts every member in memory and reports each key and its violated shape, never a value, and a concrete (non-placeholder) value of a `*.env.example` parses the same way."
4160
+ law: "A stored environment value parses as the type its `EnvSource` reader declares (an integer, `true`/`false`, an absolute URL, a duration `250`/`30s`/`5m`/`2h`, a member of the declared enum, a nonempty string or secret): `starci app secret verify --env <env>` decrypts every member in memory and reports each key and its violated shape, never a value, and a concrete (non-placeholder) value of a `*.env.example` parses the same way."
4161
4161
  kinds: [check]
4162
4162
  gates: [settle, land, ci]
4163
4163
  failureCodes: ["ENV_VALUE_SHAPE"]
@@ -5242,7 +5242,7 @@ rules:
5242
5242
  - {path: packages/hfs/src/mcp-tools-snapshot.spec.mjs, editions: [lite, full], codes: [HFS_MCP_TOOLS_SNAPSHOT]}
5243
5243
  - id: R303
5244
5244
  code: "HFS_BYPASS_MARKER"
5245
- law: "A product repository holds no bypass of a check (owner ruling loosening-always-red): no inline disable of any tool (eslint, stylelint, tslint, prettier, biome), no ts-ignore, ts-expect-error or ts-nocheck directive, no Sonar exclusion comment, no istanbul, c8 or v8 coverage ignore, no skipped, focused or todo test or skip option, and no failure swallowed in a hook, script or workflow (`|| true`, `; true`, `continue-on-error`, `set +e`, `HUSKY=0`, `--no-verify`, `fail_ci_if_error: false`, `--passWithNoTests`, `--coverageThreshold`, `--no-error-on-unmatched-pattern`, a `--max-warnings` budget). A marker is read where it acts: a suppression word opens a comment, a test marker stands in code, a swallowed failure stands in a shell line or in the string a script hands a shell; the same words quoted as data are no bypass. The runtime tree is judged by the same library (R236). A front end has no specs, but its source is still judged for the inline disables. `starci app check` reads the tracked tree and `starci app hygiene` (the managed pre-commit hook) the staged files from the index, so the bypass fails at the commit."
5245
+ law: "A product repository holds no bypass of a check (owner ruling loosening-always-red): no inline disable of any tool (eslint, stylelint, tslint, prettier, biome), no ts-ignore, ts-expect-error or ts-nocheck directive, no Sonar exclusion comment, no istanbul, c8 or v8 coverage ignore, no skipped, focused or todo test or skip option, and no failure swallowed in a hook, script or workflow (`|| true`, `; true`, `continue-on-error`, `set +e`, `HUSKY=0`, `--no-verify`, `fail_ci_if_error: false`, `--passWithNoTests`, `--coverageThreshold`, `--no-error-on-unmatched-pattern`, a `--max-warnings` budget). A marker is read where it acts: a suppression word opens a comment, a test marker stands in code, a swallowed failure stands in a shell line or in the string a script hands a shell; the same words quoted as data are no bypass. The runtime tree is judged by the same library (R236). A front end has no specs, but its source is still judged for the inline disables. `starci app check` reads the tracked tree and `starci app check --staged` (the managed pre-commit hook) the staged files from the index, so the bypass fails at the commit."
5246
5246
  kinds: [check]
5247
5247
  gates: [pre-commit, land, ci]
5248
5248
  failureCodes: ["HFS_BYPASS_MARKER"]
@@ -5803,7 +5803,7 @@ rules:
5803
5803
  ruling: "The lite database is self-hosted PostgreSQL in the app stack; its migrations live in db/migrations and starci app db migrate applies them."
5804
5804
  - id: R333
5805
5805
  code: "BE_ENTITY_SCHEMA_DRIFT"
5806
- law: "Every TypeORM entity column of an app's entity list (be/src/modules/platform/database/entities.ts) matches the schema its SQL migrations build: `starci app db drift` applies db/bootstrap.sql and every active db/migrations/*.sql to a scratch database (a container of the app's db image, or the empty database --url-env names), reads information_schema.columns and pg_catalog, and refuses a missing table, a missing column, a type that differs once both are normalised to PostgreSQL's format_type names (an enum by its type name), and a nullability that differs. A database column no entity maps is fine. A host that cannot provide the scratch database gets no verdict (exit 2, needs a database), never a green."
5806
+ law: "Every TypeORM entity column of an app's entity list (be/src/modules/platform/database/entities.ts) matches the schema its SQL migrations build: `starci app db drift` applies db/bootstrap.sql and every active db/migrations/*.sql to a scratch database (a container of the app's db image, or the empty database --db-url-env names), reads information_schema.columns and pg_catalog, and refuses a missing table, a missing column, a type that differs once both are normalised to PostgreSQL's format_type names (an enum by its type name), and a nullability that differs. A database column no entity maps is fine. A host that cannot provide the scratch database gets no verdict (exit 2, needs a database), never a green."
5807
5807
  kinds: [check]
5808
5808
  gates: [ci]
5809
5809
  failureCodes: ["BE_ENTITY_SCHEMA_DRIFT"]
@@ -5890,3 +5890,92 @@ rules:
5890
5890
  - {path: packages/eslint-canon-be/src/event-bus-lite.spec.ts, edition: lite, codes: [BE_EVENT_BUS_LITE_SHAPE]}
5891
5891
  witness:
5892
5892
  - {path: packages/eslint-canon-be/src/event-bus-lite.spec.ts, edition: lite, codes: [BE_EVENT_BUS_LITE_SHAPE]}
5893
+ - id: R337
5894
+ code: "HFS_AUTH_COOKIE_DOMAIN_MISSING"
5895
+ law: "In a lite app whose api uses the skeleton's passport (a tracked src/modules/integrations/passport/passport.config.ts), a docker-swarm environment whose rendered stack serves the web and the api on different hosts gives the api an AUTH_COOKIE_DOMAIN: a declared value in services.api.environment, a row of the environment's runtime/env/KEYS.md or a key sealed under its secrets/. Otherwise the refresh cookie is host-only on the api host, is absent from the Cookie header the web server forwards to the api, the api refuses the session and the user loops back to sign-in. The hosts are read from the rendered stack (web: NEXT_PUBLIC_SITE_URL, AUTH_WEB_ORIGIN, deploy.verify.http, HTTP_SECURITY_ALLOWED_ORIGINS; api: AUTH_JWT_ISSUER, GOOGLE_CALLBACK_URL, NEXT_PUBLIC_API_URL, deploy.verify.http), ports ignored, an unknown host is not judged. The finding names both hosts and their common parent domain as the value to set. A declared AUTH_COOKIE_DOMAIN that is not a bare domain (no scheme, port, path, leading dot or IP) or is not a parent of the web and the api hosts is a finding too. Same host: no key is required."
5896
+ kinds: [check]
5897
+ gates: [settle, land, ci]
5898
+ failureCodes: ["HFS_AUTH_COOKIE_DOMAIN_MISSING"]
5899
+ enforcers:
5900
+ - {kind: hfs, id: stacks-auth-cookie-domain, at: scripts/hfs/rules/stacks-auth-cookie-domain.mjs}
5901
+ editions: [lite]
5902
+ docs:
5903
+ - contexts/project/stacks.md
5904
+ proves:
5905
+ - {path: packages/hfs/src/hfs-stacks-auth-cookie-domain.spec.mjs, edition: lite}
5906
+ exemplifiedBy:
5907
+ - {path: packages/hfs/templates/app/skeleton-lite/.starcistacks, edition: lite}
5908
+ - {path: packages/hfs/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.ts, edition: lite}
5909
+ - {path: packages/hfs/src/hfs-stacks-auth-cookie-domain.spec.mjs, edition: lite}
5910
+ witness:
5911
+ - {path: packages/hfs/src/hfs-stacks-auth-cookie-domain.spec.mjs, edition: lite}
5912
+
5913
+ - id: R338
5914
+ code: "FE_GENERATED_ART_OUTSIDE_FRAME"
5915
+ law: "Generated art, a .webp or .png under the public /images/ tree, renders only through Grammar's Mascot (the character size scale sm, md, lg, hero) or inside a MediaFrame; a bare Image, img or other src-taking element drawing it takes whatever box its page gives, so the same art comes out at a different size on every screen."
5916
+ kinds: [lint]
5917
+ gates: [pre-commit, settle, land, ci]
5918
+ failureCodes: ["FE_GENERATED_ART_OUTSIDE_FRAME"]
5919
+ enforcers:
5920
+ - {kind: eslint-grammar-core, id: generated-art-in-mascot-or-media-frame}
5921
+ editions: [lite, full]
5922
+ docs: [knowledge/patterns/fe/ui.yaml#FE-UI]
5923
+ templateNa: "A template draws no generated art; the art is the app's."
5924
+ proves:
5925
+ - packages/eslint-grammar-core/src/mascot.spec.ts
5926
+ exemplifiedBy:
5927
+ - {path: packages/eslint-grammar-core/src/mascot.spec.ts, editions: [lite, full], codes: [FE_GENERATED_ART_OUTSIDE_FRAME]}
5928
+ witness:
5929
+ - {path: packages/eslint-grammar-core/src/mascot.spec.ts, editions: [lite, full], codes: [FE_GENERATED_ART_OUTSIDE_FRAME]}
5930
+ cases:
5931
+ - id: case-1
5932
+ date: "2026-10-30"
5933
+ source: owner
5934
+ quote: "Generated art renders at one consistent size across screens (relayed by the nivo-lite app)."
5935
+ ruling: "Grammar's Mascot owns the character size scale (sm 96px, md 160px, lg 240px, hero up to its cap), always contained on a transparent ground; generated art under /images/ renders through Mascot or inside MediaFrame, never a bare Image."
5936
+ - id: R339
5937
+ code: "FE_MASCOT_ASSET_SLOT_MISSING"
5938
+ law: "Every Grammar Mascot names its assetSlot (a non-empty value), tying the rendered art to the asset workflow's request; an element with a spread attribute is not judged."
5939
+ kinds: [lint]
5940
+ gates: [pre-commit, settle, land, ci]
5941
+ failureCodes: ["FE_MASCOT_ASSET_SLOT_MISSING"]
5942
+ enforcers:
5943
+ - {kind: eslint-grammar-core, id: mascot-asset-slot-required}
5944
+ editions: [lite, full]
5945
+ docs: [knowledge/patterns/fe/ui.yaml#FE-UI]
5946
+ templateNa: "A template draws no mascot; the art is the app's."
5947
+ proves:
5948
+ - packages/eslint-grammar-core/src/mascot.spec.ts
5949
+ exemplifiedBy:
5950
+ - {path: packages/eslint-grammar-core/src/mascot.spec.ts, editions: [lite, full], codes: [FE_MASCOT_ASSET_SLOT_MISSING]}
5951
+ witness:
5952
+ - {path: packages/eslint-grammar-core/src/mascot.spec.ts, editions: [lite, full], codes: [FE_MASCOT_ASSET_SLOT_MISSING]}
5953
+ cases:
5954
+ - id: case-1
5955
+ date: "2026-10-30"
5956
+ source: owner
5957
+ quote: "A Mascot reuses the provenance props of Image and MediaFrame; a Mascot without an assetSlot is a finding (relayed by the nivo-lite app)."
5958
+ ruling: "Mascot takes AssetSlotProps (assetSlot, assetSha256, assetPrompt) and emits them as data-asset-* on its root; the lint requires assetSlot on every Mascot."
5959
+ - id: R340
5960
+ code: "FE_RAISED_SURFACE_IN_SURFACE"
5961
+ law: "A Grammar surface that accepts a depth prop (SurfaceListCard) does not pass depth=\"top\" when a SurfaceCard, SurfaceListCard or SurfaceAccordionCard of the same file encloses it: a nested surface is subordinate (outlined, BOUNDARY-5), never raised again (BOUNDARY-6, DNA-COMMON-NO-RAISED-SURFACE-IN-SURFACE); the Grammar resolves the depth from the surface context. A depth computed at run time is not judged."
5962
+ kinds: [lint]
5963
+ gates: [pre-commit, settle, land, ci]
5964
+ failureCodes: ["FE_RAISED_SURFACE_IN_SURFACE"]
5965
+ enforcers:
5966
+ - {kind: eslint-grammar-core, id: no-raised-surface-in-surface}
5967
+ editions: [lite, full]
5968
+ docs: [knowledge/patterns/fe/ui.yaml#FE-UI]
5969
+ templateNa: "A template forces no surface depth; the app composes its surfaces."
5970
+ proves:
5971
+ - packages/eslint-grammar-core/src/surface-depth.spec.ts
5972
+ exemplifiedBy:
5973
+ - {path: packages/eslint-grammar-core/src/surface-depth.spec.ts, editions: [lite, full], codes: [FE_RAISED_SURFACE_IN_SURFACE]}
5974
+ witness:
5975
+ - {path: packages/eslint-grammar-core/src/surface-depth.spec.ts, editions: [lite, full], codes: [FE_RAISED_SURFACE_IN_SURFACE]}
5976
+ cases:
5977
+ - id: case-1
5978
+ date: "2026-10-31"
5979
+ source: owner
5980
+ quote: "A list inside another surface is outlined (BOUNDARY-5, DNA-COMMON-NO-RAISED-SURFACE-IN-SURFACE); SurfaceListCard takes its depth from the surface context (grammar 0.16.2)."
5981
+ ruling: "SurfaceListCard resolves its depth from the surface context like SurfaceCard and SurfaceAccordionCard; an app that forces depth=\"top\" on a surface nested in another surface raises a surface inside a surface, so the lint refuses it."