@starci/hfs 4.8.0 → 4.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/emit/operation-schemas.mjs +3 -0
  3. package/lint/bound-sys.cjs +2 -2
  4. package/package.json +1 -1
  5. package/runtime/engine/admission.mjs +1 -1
  6. package/runtime/engine/config.mjs +2 -2
  7. package/runtime/engine/db/machine.mjs +2 -3
  8. package/runtime/engine/invalid-config.mjs +1 -1
  9. package/runtime/knowledge/hfs/canon-pins.yaml +22 -14
  10. package/runtime/knowledge/hfs/rules.yaml +23 -0
  11. package/runtime/modules/kernel/failure-codes.yaml +10 -0
  12. package/runtime/scripts/guards/git-policy.mjs +2 -3
  13. package/runtime/scripts/hfs/rules/pipeline.mjs +5 -5
  14. package/runtime/scripts/lib/package-at.mjs +5 -6
  15. package/runtime/scripts/lib/roots.mjs +2 -2
  16. package/runtime/scripts/work/draw/draw-dna-findings.mjs +23 -12
  17. package/runtime/scripts/work/draw/draw-dna.mjs +14 -8
  18. package/scaffold/app.mjs +4 -4
  19. package/src/main.mjs +3 -3
  20. package/sync/index.mjs +14 -1
  21. package/sync/managed.mjs +3 -2
  22. package/templates/app/codegen-graphql.mjs.partial +4 -4
  23. package/templates/app/quality-config/sonar-project.properties +4 -2
  24. package/runtime/engine/canonical-json.mjs +0 -14
  25. package/runtime/engine/db/ledger-open.mjs +0 -38
  26. package/runtime/engine/db/ledger-paths.mjs +0 -87
  27. package/runtime/engine/db/ledger.mjs +0 -1068
  28. package/runtime/engine/db/provision-authority.mjs +0 -76
  29. package/runtime/engine/db/provision-operations.mjs +0 -146
  30. package/runtime/packages/grammar/scripts/build-stamp.mjs +0 -98
  31. package/runtime/scripts/agent/codex-app-server.mjs +0 -79
  32. package/runtime/scripts/agent/codex-mirror-source.mjs +0 -34
  33. package/runtime/scripts/agent/host-agents.mjs +0 -80
  34. package/runtime/scripts/agent/launch-trust-policy.mjs +0 -53
  35. package/runtime/scripts/agent/trust-launch.mjs +0 -191
  36. package/runtime/scripts/agent/trust.mjs +0 -523
  37. package/runtime/scripts/api/fs/temp-path.mjs +0 -8
  38. package/runtime/scripts/api/git/diff-names.mjs +0 -8
  39. package/runtime/scripts/api/git/head-time.mjs +0 -10
  40. package/runtime/scripts/api/git/index-lock.mjs +0 -78
  41. package/runtime/scripts/api/git/log.mjs +0 -5
  42. package/runtime/scripts/api/git/merge-tree.mjs +0 -5
  43. package/runtime/scripts/api/git/rev-list.mjs +0 -5
  44. package/runtime/scripts/api/git/rev-parse.mjs +0 -13
  45. package/runtime/scripts/api/git/status-query.mjs +0 -5
  46. package/runtime/scripts/api/git/worktree-list-query.mjs +0 -5
  47. package/runtime/scripts/api/node/spawn-node.mjs +0 -6
  48. package/runtime/scripts/api/npm/dist-tag.mjs +0 -8
  49. package/runtime/scripts/api/npm/lib.mjs +0 -32
  50. package/runtime/scripts/api/npm/pack-dry-run.mjs +0 -9
  51. package/runtime/scripts/api/npm/run-npm.mjs +0 -6
  52. package/runtime/scripts/api/npm/view.mjs +0 -30
  53. package/runtime/scripts/api/npm/whoami.mjs +0 -8
  54. package/runtime/scripts/api/process/hide-child-windows.mjs +0 -31
  55. package/runtime/scripts/api/process/kill-tree.mjs +0 -22
  56. package/runtime/scripts/api/process/lib.mjs +0 -125
  57. package/runtime/scripts/api/process/probe-guard-command.mjs +0 -74
  58. package/runtime/scripts/api/process/process-list.mjs +0 -28
  59. package/runtime/scripts/api/process/system-tool.mjs +0 -37
  60. package/runtime/scripts/gates/canon-digest.mjs +0 -81
  61. package/runtime/scripts/gates/canon-pins.mjs +0 -119
  62. package/runtime/scripts/gates/gate-input.mjs +0 -98
  63. package/runtime/scripts/gates/gate.mjs +0 -636
  64. package/runtime/scripts/gates/grammar-dist.mjs +0 -206
  65. package/runtime/scripts/gates/output.mjs +0 -40
  66. package/runtime/scripts/gates/runtime-package-selection.mjs +0 -27
  67. package/runtime/scripts/gates/test-world-run.mjs +0 -252
  68. package/runtime/scripts/gates/type-impact.mjs +0 -277
  69. package/runtime/scripts/gates/type-program.mjs +0 -113
  70. package/runtime/scripts/guards/assigned-command.mjs +0 -12
  71. package/runtime/scripts/guards/call-rights.mjs +0 -47
  72. package/runtime/scripts/guards/command-guard.mjs +0 -468
  73. package/runtime/scripts/guards/command-policy.mjs +0 -342
  74. package/runtime/scripts/guards/critic-reach.mjs +0 -82
  75. package/runtime/scripts/guards/deps-guard.mjs +0 -150
  76. package/runtime/scripts/guards/env-dump-verdict.mjs +0 -89
  77. package/runtime/scripts/guards/guards-root.mjs +0 -27
  78. package/runtime/scripts/guards/hook-install.mjs +0 -343
  79. package/runtime/scripts/guards/hook-verdict.mjs +0 -12
  80. package/runtime/scripts/guards/install-verdict.mjs +0 -41
  81. package/runtime/scripts/guards/kernel-seat.mjs +0 -63
  82. package/runtime/scripts/guards/launch-verdict.mjs +0 -74
  83. package/runtime/scripts/guards/op-starci.mjs +0 -16
  84. package/runtime/scripts/guards/op-write-scope.mjs +0 -27
  85. package/runtime/scripts/guards/orca-self-lifecycle.mjs +0 -39
  86. package/runtime/scripts/guards/process-kill-verdict.mjs +0 -106
  87. package/runtime/scripts/guards/protected-zone.mjs +0 -56
  88. package/runtime/scripts/guards/refusals.mjs +0 -27
  89. package/runtime/scripts/guards/rights.mjs +0 -268
  90. package/runtime/scripts/guards/seat-tools.mjs +0 -47
  91. package/runtime/scripts/guards/shell-commands.mjs +0 -328
  92. package/runtime/scripts/guards/supervisor-seat.mjs +0 -80
  93. package/runtime/scripts/guards/verdict-process.mjs +0 -81
  94. package/runtime/scripts/lib/app-data.mjs +0 -11
  95. package/runtime/scripts/lib/canon-binding-text.mjs +0 -129
  96. package/runtime/scripts/lib/check-format.mjs +0 -7
  97. package/runtime/scripts/lib/failure-code-reads.mjs +0 -100
  98. package/runtime/scripts/lib/failure-code-template-errors.mjs +0 -161
  99. package/runtime/scripts/lib/git-dir.mjs +0 -120
  100. package/runtime/scripts/lib/guard-command.mjs +0 -24
  101. package/runtime/scripts/lib/interrupt-cleanup.mjs +0 -38
  102. package/runtime/scripts/lib/json-formats.mjs +0 -34
  103. package/runtime/scripts/lib/managed-json.mjs +0 -51
  104. package/runtime/scripts/lib/menu-parts.mjs +0 -11
  105. package/runtime/scripts/lib/op-gate.mjs +0 -15
  106. package/runtime/scripts/lib/provision-control-failure.mjs +0 -32
  107. package/runtime/scripts/lib/release-version.mjs +0 -134
  108. package/runtime/scripts/lib/runtime-location.mjs +0 -21
  109. package/runtime/scripts/lib/runtime-package-inventory.mjs +0 -21
  110. package/runtime/scripts/lib/runtime-package-manifests.mjs +0 -84
  111. package/runtime/scripts/lib/runtime-publication-tags.mjs +0 -56
  112. package/runtime/scripts/lib/seat-env.mjs +0 -43
  113. package/runtime/scripts/lib/sonar-new-code.mjs +0 -58
  114. package/runtime/scripts/lib/source-package-imports.mjs +0 -111
  115. package/runtime/scripts/lib/tar-files.mjs +0 -39
  116. package/runtime/scripts/lib/uat-run-folder.mjs +0 -118
  117. package/runtime/scripts/lib/worktree-record.mjs +0 -14
  118. package/runtime/scripts/machine/ask-receipts.mjs +0 -113
  119. package/runtime/scripts/machine/evidence-store.mjs +0 -223
  120. package/runtime/scripts/machine/home.mjs +0 -216
  121. package/runtime/scripts/machine/host-lock.mjs +0 -184
  122. package/runtime/scripts/machine/lock-recovery.mjs +0 -55
  123. package/runtime/scripts/machine/roles-contract.mjs +0 -90
  124. package/runtime/scripts/machine/runtime-change.mjs +0 -20
  125. package/runtime/scripts/machine/workflow-tree.mjs +0 -122
  126. package/runtime/scripts/reconciler/checklist-items.mjs +0 -7
  127. package/runtime/scripts/reconciler/guard-row.mjs +0 -25
  128. package/runtime/scripts/work/brand/brand-colour.mjs +0 -125
  129. package/runtime/scripts/work/brand/brand-tokens.mjs +0 -167
  130. package/runtime/scripts/work/brand/brand-value-source.mjs +0 -106
  131. package/runtime/scripts/work/brand/brand.mjs +0 -892
  132. package/runtime/scripts/work/draw/draw-layer.mjs +0 -306
  133. package/runtime/scripts/work/draw/draw-rationale-page.mjs +0 -237
  134. package/runtime/scripts/work/draw/draw-rationale-ref.mjs +0 -14
  135. package/runtime/scripts/work/draw/draw-rationale.mjs +0 -353
  136. package/runtime/scripts/work/draw/draw-source.mjs +0 -412
  137. package/runtime/scripts/work/draw/draw-taste.mjs +0 -278
  138. package/runtime/scripts/work/draw/draw-typecheck.mjs +0 -72
  139. package/runtime/scripts/work/draw/rationale-findings.mjs +0 -205
  140. package/runtime/scripts/work/draw-grammar.mjs +0 -175
  141. package/runtime/scripts/work/draw-render-page.mjs +0 -188
  142. package/runtime/scripts/work/draw-render.mjs +0 -626
  143. package/runtime/scripts/work/png.mjs +0 -237
  144. package/runtime/scripts/work/render-tools.mjs +0 -64
@@ -1,206 +0,0 @@
1
- // grammar-dist.mjs - is packages/grammar/dist the build of the source beside it? The judge every grammar dist reader asks
2
- // (gates, work, the reconciler and land); scripts/checks/check-grammar-dist.mjs is its self-check CLI.
3
- import fs from 'node:fs';
4
- import path from 'node:path';
5
- import { byCodeUnit } from '../lib/list.mjs';
6
- import {skillRoot} from '../../engine/runtime-root.mjs';
7
- import {gitDirOf} from '../lib/git-dir.mjs';
8
- import {readJsonFile as readJson} from '../lib/json.mjs';
9
- import {slash} from '../lib/path-key.mjs';
10
- import {DIGEST_ALGORITHM,STAMP_FILE,STAMP_SCHEMA,distDigest,sourceDigest} from '../../packages/grammar/scripts/build-stamp.mjs';
11
-
12
- /**
13
- * A built `packages/grammar/dist` is what products actually get: `file:` consumers link the package
14
- * directory, and the brand check reads the exported CSS. `dist/` is not tracked, so nothing but a build
15
- * refreshes it, and a merge that changes the source leaves the previous build in place. This module
16
- * decides whether a dist is the build of the source beside it, and every reader of grammar dist asks it
17
- * first and refuses a dist it does not call fresh.
18
- *
19
- * Fresh means: `dist/.build-stamp.json` exists, names the manifest's version, was made with this
20
- * digest algorithm from the current source digest, the built files still hash to the stamp's dist
21
- * digest, and every `--*` custom property the source CSS declares has the same value in the copied dist
22
- * CSS. A package that does not carry its source (a registry install) cannot be rebuilt or compared, so
23
- * it is `unverifiable` and allowed once its stamp binds: the version and dist digest must agree.
24
- */
25
-
26
- export const GRAMMAR_PACKAGE='@starci/grammar';
27
- export const GRAMMAR_DIST_FIX='run npm run build in packages/grammar';
28
- const GRAMMAR_DIST_CHECK='starci/grammar-dist-check@1';
29
- const TOKEN_DIFF_CAP=20;
30
- const CSS_TOKEN_NAME = '--[A-Za-z0-9_-]+';
31
- const CSS_TOKEN_SPACE=String.raw`\s*`;
32
- const CSS_TOKEN_VALUE = '[^;{}]*';
33
- const CSS_TOKEN_DECLARATION=new RegExp(`(${CSS_TOKEN_NAME})${CSS_TOKEN_SPACE}:(${CSS_TOKEN_VALUE})`,'g');
34
-
35
-
36
- /**
37
- * The primary worktree of the checkout `dir` belongs to, or null when `dir` is the primary itself or carries no
38
- * linked-worktree metadata. A linked worktree's `.git` file names `<primary>/.git/worktrees/<name>`, and that
39
- * directory's `commondir` file names `<primary>/.git` — read from disk, never a git spawn.
40
- */
41
- const primaryWorktreeOf=(dir)=>{
42
- const gitDir=gitDirOf(dir);
43
- if(!gitDir||gitDir===path.join(path.resolve(dir),'.git'))return null;
44
- try{
45
- const common=path.resolve(gitDir,fs.readFileSync(path.join(gitDir,'commondir'),'utf8').trim());
46
- return path.basename(common)==='.git'?path.dirname(common):null;
47
- }catch{return null;}
48
- };
49
-
50
- /**
51
- * The grammar package the runtime at `runtimeRoot` ships: its own packages/grammar while that carries a dist/, or the
52
- * primary worktree's when this checkout is a linked worktree that never built one — dist/ is untracked, so a land
53
- * scratch or a lane checkout shares the primary's build (draw-grammar.mjs resolves a lane's grammar the same way).
54
- * The primary worktree itself, and any standalone checkout, always answer their own: a missing dist on live main
55
- * stays `missing`.
56
- */
57
- function shippedGrammarPackageRoot(runtimeRoot=skillRoot,deps={}){
58
- const own=path.join(runtimeRoot,'packages','grammar');
59
- if((deps.exists??fs.existsSync)(path.join(own,'dist')))return own;
60
- const primary=(deps.primaryOf??primaryWorktreeOf)(runtimeRoot);
61
- return primary?path.join(primary,'packages','grammar'):own;
62
- }
63
-
64
- export const defaultGrammarPackageRoot=()=>shippedGrammarPackageRoot(skillRoot);
65
-
66
- /** Every `--name: value` declaration outside a comment, in document order, whitespace collapsed. */
67
- function cssTokenDeclarations(text){
68
- const code=String(text).replace(/\/\*[\s\S]*?\*\//g,'');
69
- return [...code.matchAll(CSS_TOKEN_DECLARATION)].map(m=>({name:m[1],value:m[2].trim().replace(/\s+/g,' ')}));
70
- }
71
-
72
- /** The families `packages/grammar/scripts/copy-css.mjs` copies: every `src/<family>` that has a `styles.css`. */
73
- function cssFamilies(packageRoot){
74
- const src=path.join(packageRoot,'src');
75
- if(!fs.existsSync(src))return [];
76
- return fs.readdirSync(src,{withFileTypes:true}).filter(entry=>entry.isDirectory()&&fs.existsSync(path.join(src,entry.name,'styles.css')))
77
- .map(entry=>entry.name).sort(byCodeUnit);
78
- }
79
-
80
- /**
81
- * Compares every family's `--*` token declarations in dist CSS with the source CSS they were copied from.
82
- * Returns the differing tokens (capped) and how many declarations were compared.
83
- */
84
- function compareCssTokens(packageRoot){
85
- const differences=[];let compared=0;
86
- for(const family of cssFamilies(packageRoot)){
87
- const dir=path.join(packageRoot,'src',family);
88
- for(const name of fs.readdirSync(dir).filter(file=>file.endsWith('.css')).sort()){
89
- const file=`${family}/${name}`;
90
- const built=path.join(packageRoot,'dist',family,name);
91
- const source=cssTokenDeclarations(fs.readFileSync(path.join(dir,name),'utf8'));
92
- if(!fs.existsSync(built)){differences.push({file,token:null,src:`${source.length} declarations`,dist:'file missing'});continue;}
93
- const dist=cssTokenDeclarations(fs.readFileSync(built,'utf8'));
94
- compared+=source.length;
95
- const group=list=>list.reduce((map,{name:token,value})=>map.set(token,[...(map.get(token)??[]),value]),new Map());
96
- const [want,have]=[group(source),group(dist)];
97
- for(const token of [...new Set([...want.keys(),...have.keys()])].sort(byCodeUnit)){
98
- const [a,b]=[want.get(token)??[],have.get(token)??[]];
99
- if(a.join('\0')!==b.join('\0'))differences.push({file,token,src:a.join(' | ')||null,dist:b.join(' | ')||null});
100
- }
101
- }
102
- }
103
- return {compared,differences:differences.slice(0,TOKEN_DIFF_CAP),differenceCount:differences.length};
104
- }
105
-
106
- /**
107
- * The nearest package that owns `file` when it is `@starci/grammar`, with whether the file is under its
108
- * `dist/`. Symlinks are resolved first, so `node_modules/@starci/grammar` of a `file:` consumer lands on
109
- * the linked package directory.
110
- */
111
- function grammarPackageOf(file){
112
- let real;
113
- try{real=fs.realpathSync(path.resolve(file));}catch{return null;}
114
- for(let cursor=path.dirname(real);path.dirname(cursor)!==cursor;cursor=path.dirname(cursor)){
115
- const manifest=path.join(cursor,'package.json');
116
- if(!fs.existsSync(manifest))continue;
117
- if(readJson(manifest)?.name!==GRAMMAR_PACKAGE)return null;
118
- const relative=slash(path.relative(cursor,real));
119
- return {root:cursor,file:real,inDist:relative==='dist'||relative.startsWith('dist/')};
120
- }
121
- return null;
122
- }
123
-
124
- /**
125
- * The freshness of one grammar package's dist. `state` is one of fresh, unverifiable (no source carried,
126
- * nothing to compare), missing, unstamped, stale, tampered; `ok` is true only for the first two.
127
- */
128
- /** The `; <token summary>` tail of a detail string, empty when no token differs. */
129
- const tokenTail=tokens=>tokens.differenceCount?'; '+tokenSummary(tokens):'';
130
-
131
- /** The result for a dist with no valid stamp. */
132
- const unstampedResult=(result,tokens,evidence,stampFile)=>
133
- result('unstamped',`dist/${STAMP_FILE} is ${fs.existsSync(stampFile)?'not a valid build stamp':'missing'}, so this dist was not produced by the current build script${tokenTail(tokens)}`,evidence);
134
-
135
- /** The result when the stamp's algorithm or source digest no longer binds the current source, or null. */
136
- const sourceStaleness=(root,stamp,tokens,evidence,result)=>{
137
- if(stamp.algorithm!==DIGEST_ALGORITHM)
138
- return result('stale',`the stamp was made with digest ${stamp.algorithm ?? '(none)'}, this check computes ${DIGEST_ALGORITHM}`,evidence);
139
- const current=sourceDigest(root);
140
- if(stamp.sourceDigest!==current)
141
- return result('stale',`dist was built from source ${short(stamp.sourceDigest)} (${stamp.version}, ${stamp.builtAt}) but the source is now ${short(current)}${tokenTail(tokens)}`,{...evidence,sourceDigest:current});
142
- return null;
143
- };
144
-
145
- /** The result when the stamp's version/dist digest no longer binds the package, or null. */
146
- const bindingStaleness=(root,manifest,stamp,tokens,evidence,result)=>{
147
- if(stamp.version!==manifest.version)
148
- return result('stale',`dist was built as version ${stamp.version} but package.json is ${manifest.version}`,evidence);
149
- const built=distDigest(root);
150
- if(stamp.distDigest!==built)
151
- return result('tampered',`dist files changed after the build (stamp ${short(stamp.distDigest)}, now ${short(built)})${tokenTail(tokens)}`,{...evidence,distDigest:built});
152
- if(tokens.differenceCount)return result('tampered',tokenSummary(tokens),evidence);
153
- return null;
154
- };
155
-
156
- export function grammarDistStatus(packageRoot=defaultGrammarPackageRoot()){
157
- const root=path.resolve(packageRoot);
158
- const manifest=readJson(path.join(root,'package.json'));
159
- const base={schema:GRAMMAR_DIST_CHECK,root:slash(root),package:manifest?.name??null,version:manifest?.version??null,fix:GRAMMAR_DIST_FIX};
160
- const result=(state,detail,extra={})=>({...base,state,ok:state==='fresh'||state==='unverifiable',detail,...extra});
161
- if(!manifest)return result('missing',`${slash(root)}/package.json is missing or unreadable`);
162
- const hasSource=fs.existsSync(path.join(root,'src'));
163
- const distDir=path.join(root,'dist');
164
- if(!fs.existsSync(distDir)||!fs.statSync(distDir).isDirectory())
165
- return hasSource?result('missing','dist/ does not exist; the package has never been built here'):result('unverifiable','no dist/ and no source: nothing to verify');
166
- const stampFile=path.join(distDir,STAMP_FILE);
167
- const stamp=readJson(stampFile);
168
- const tokens=hasSource?compareCssTokens(root):{compared:0,differences:[],differenceCount:0};
169
- const evidence={stamp,tokens};
170
- if(!stamp||stamp.schema!==STAMP_SCHEMA)return unstampedResult(result,tokens,evidence,stampFile);
171
- if(hasSource){
172
- const stale=sourceStaleness(root,stamp,tokens,evidence,result);
173
- if(stale)return stale;
174
- }
175
- const bound=bindingStaleness(root,manifest,stamp,tokens,evidence,result);
176
- if(bound)return bound;
177
- return hasSource
178
- ?result('fresh',`${manifest.name}@${manifest.version} built from source ${short(stamp.sourceDigest)} at ${stamp.builtAt}; ${tokens.compared} token declarations match src`,evidence)
179
- :result('unverifiable',`${manifest.name}@${manifest.version} carries no source; its stamp and built files agree`,evidence);
180
- }
181
-
182
- const short=digest=>typeof digest==='string'?digest.replace(/^sha256:/,'').slice(0,12):'(none)';
183
- const tokenDifferenceText=d=>{
184
- if(!d.token)return `${d.file} ${d.dist}`;
185
- return `${d.file} ${d.token} src=${d.src??'(absent)'} dist=${d.dist??'(absent)'}`;
186
- };
187
- function tokenSummary(tokens){
188
- const shown=tokens.differences.slice(0,3).map(tokenDifferenceText);
189
- return `${tokens.differenceCount} dist CSS token value(s) differ from src: ${shown.join('; ')}${tokens.differenceCount>3?'; ...':''}`;
190
- }
191
-
192
- /** One line a reader throws or reports when it refuses a dist. Ends with the exact fix. */
193
- export function grammarDistMessage(status){
194
- return `${status.package??GRAMMAR_PACKAGE} dist at ${status.root}/dist is ${status.state}: ${status.detail}. Fix: ${GRAMMAR_DIST_FIX}.`;
195
- }
196
-
197
- /**
198
- * The refusal for a file a reader is about to read: null when the file is not in a grammar dist or the
199
- * dist is fresh (or unverifiable), otherwise the status with its message.
200
- */
201
- export function grammarDistRefusal(file){
202
- const owner=grammarPackageOf(file);
203
- if(!owner?.inDist)return null;
204
- const status=grammarDistStatus(owner.root);
205
- return status.ok?null:{...status,message:grammarDistMessage(status)};
206
- }
@@ -1,40 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import {redactBytes} from '../lib/redact.mjs';
4
-
5
- /** Print a gate summary, retain an optional scratch copy, and return its measured exit code. */
6
- export function emitGateSummary(summary, out = null, write = text => process.stdout.write(text)) {
7
- const text = `${JSON.stringify(summary, null, 2)}\n`;
8
- if (out) {
9
- const file = path.resolve(out);
10
- fs.mkdirSync(path.dirname(file), {recursive: true});
11
- fs.writeFileSync(file, text);
12
- }
13
- write(text);
14
- return summary.exit;
15
- }
16
-
17
- /**
18
- * Write a check result to scratch, stdout, or the external content-addressed store. A blob is redacted first when its
19
- * media type is text (scripts/lib/redact.mjs); the printed {sha, redaction} is what a check_runs row cites.
20
- */
21
- export async function emitCheckOutput(value, {out = null, blob = false, mediaType = 'application/json',
22
- write = text => process.stdout.write(text), put = null} = {}) {
23
- if (out && blob) throw new Error('--out and --blob are mutually exclusive');
24
- const bytes = Buffer.isBuffer(value) ? value : Buffer.from(String(value));
25
- if (out) {
26
- const file = path.resolve(out);
27
- fs.mkdirSync(path.dirname(file), {recursive: true});
28
- fs.writeFileSync(file, bytes);
29
- return {out: file};
30
- }
31
- if (blob) {
32
- const putBlob = put ?? (await import('../../engine/db/blob.mjs')).putBlob;
33
- const clean = redactBytes(bytes, mediaType);
34
- const {sha} = await putBlob(clean.bytes, {mediaType});
35
- write(`${JSON.stringify({sha, redaction: clean.redaction})}\n`);
36
- return {sha, redaction: clean.redaction};
37
- }
38
- write(bytes.toString());
39
- return {};
40
- }
@@ -1,27 +0,0 @@
1
- // npm selects the root publish inventory from tracked names and verified generated names, without copying unrelated payload.
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import { containedPath } from '../lib/path-key.mjs';
5
- import { isLinkLike } from '../api/fs/is-link-like.mjs';
6
- import { packDryRun } from '../api/npm/pack-dry-run.mjs';
7
- import { runNpm } from '../api/npm/run-npm.mjs';
8
-
9
- /** npm's selected relative names over a fresh placeholder tree; only package/ignore metadata is copied from source. */
10
- export function packTrackedSelection(root, projection, { tracked, generated = [], env = process.env, run = runNpm } = {}) {
11
- fs.mkdirSync(projection);
12
- const sourceRoot = path.resolve(root);
13
- for (const relative of new Set([...tracked, ...generated])) {
14
- const target = containedPath(projection, relative);
15
- fs.mkdirSync(path.dirname(target), { recursive: true });
16
- if (['package.json', 'package-lock.json', 'npm-shrinkwrap.json', '.npmignore', '.gitignore'].includes(path.basename(relative))) {
17
- containedPath(sourceRoot, relative);
18
- const source = path.resolve(sourceRoot, relative);
19
- for (let cursor = source; cursor !== sourceRoot; cursor = path.dirname(cursor)) if (isLinkLike(cursor)) throw new Error(`tracked selection metadata contains a link: ${relative}`);
20
- fs.copyFileSync(source, target);
21
- } else fs.writeFileSync(target, '');
22
- }
23
- const listed = packDryRun(projection, { spawn: (args, options) => run(args, { ...options, env }) });
24
- if (listed.error || listed.status !== 0) throw new Error('npm could not select the tracked-only root projection');
25
- const packed = JSON.parse(listed.stdout);
26
- return Array.isArray(packed) && packed.length === 1 && Array.isArray(packed[0].files) ? packed[0].files.map((file) => file.path) : null;
27
- }
@@ -1,252 +0,0 @@
1
- #!/usr/bin/env node
2
- // test-world-run.mjs - the test-world run summary (schema starci/test-world-run@1) e2e.verify and integration.verify attach
3
- // (knowledge/op-gate.yaml proofs.test-world, contract change op-mechanism-proofs).
4
- //
5
- // starci gate test-world --root <app> --project e2e|integration|contract [--tests <path pattern>] [--out <file>]
6
- //
7
- // Over the app at --root it records, and `starci kernel settle` re-reads (scripts/kernel/gate-settle.mjs):
8
- // harness the be jest config is @starci/jest-preset's starciJestConfig() (its world projects run on the preset's world
9
- // runner: up to min(--maxWorkers, slots) files at once, each in a fresh process on its own data slot) and the world
10
- // declaration be/src/tests/world/test-world.config.ts
11
- // calls defineTestWorld;
12
- // specs every spec of the project (be/src/tests/{e2e,integration,contract}/**/*.<project>-spec.ts, narrowed by --tests)
13
- // takes its world from the library in the layer's form - useTestWorld({ apps }) for e2e, useTestWorld({ modules })
14
- // for integration, useSandbox(...) for contract (the real client against the provider's sandbox) - and hand-rolls
15
- // none of op-gate.yaml testWorld.forbidden (docker, testcontainers, DataSource, process.env, sleeps); the outage calls
16
- // (cut, restore, during, latency) are counted;
17
- // run the managed `npm run test:<project>` with jest's --json report: exit, totals, failed and skipped tests.
18
- // Our own stack runs real and only third-party SaaS is faked at the network edge: the library owns that, a spec never does.
19
- // Exit 0 green, 1 a finding or a red run, 2 the summary could not be built.
20
- import fs from 'node:fs';
21
- import path from 'node:path';
22
- import { runNpm } from '../api/npm/run-npm.mjs';
23
- import { fileURLToPath } from 'node:url';
24
- import { posixPath } from '../lib/path-key.mjs';
25
- import { valueFlags } from '../lib/cli-arg.mjs';
26
- import { isMain } from '../lib/is-main.mjs'; import { walkFiles } from '../lib/walk.mjs';
27
- import { opGateRules } from '../lib/op-gate.mjs';
28
- import { makeTempDir } from '../api/fs/make-temp-dir.mjs';
29
- import { emitGateSummary } from './output.mjs';
30
-
31
- export const TEST_WORLD_RUN_SCHEMA = 'starci/test-world-run@1';
32
- const WORLD_PROJECTS = Object.freeze({
33
- e2e: { dir: 'be/src/tests/e2e', suffix: '.e2e-spec.ts', mode: 'apps' },
34
- integration: { dir: 'be/src/tests/integration', suffix: '.integration-spec.ts', mode: 'modules' },
35
- contract: { dir: 'be/src/tests/contract', suffix: '.contract-spec.ts', mode: 'sandbox' },
36
- });
37
- const runtimeRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
38
- const JEST_CONFIGS = ['be/jest.config.js', 'be/jest.config.cjs', 'be/jest.config.ts', 'jest.config.js', 'jest.config.cjs', 'jest.config.ts'];
39
- const DECLARATION = 'be/src/tests/world/test-world.config.ts';
40
- const USAGE = 'usage: starci gate test-world --root <app> --project e2e|integration|contract [--tests <path pattern>] [--out <file>]';
41
-
42
- /** op-gate.yaml testWorld: {required[], forbidden[], outage[]}. */
43
- export function testWorldRules(runtime = runtimeRoot) {
44
- return opGateRules(runtime, 'testWorld');
45
- }
46
-
47
- /** The harness of the app: {jestConfig, preset, declaration, defineTestWorld}. */
48
- function harnessOf(root) {
49
- const jestConfig = JEST_CONFIGS.find((rel) => fs.existsSync(path.join(root, rel))) ?? null;
50
- const text = jestConfig ? fs.readFileSync(path.join(root, jestConfig), 'utf8') : '';
51
- const declared = fs.existsSync(path.join(root, DECLARATION)) ? fs.readFileSync(path.join(root, DECLARATION), 'utf8') : null;
52
- return { jestConfig, preset: /@starci\/jest-preset/.test(text) && /starciJestConfig\s*\(/.test(text), declaration: declared === null ? null : DECLARATION, defineTestWorld: /defineTestWorld\s*\(/.test(declared ?? '') };
53
- }
54
-
55
- /** The `{ ... }` argument text of each useTestWorld( call, balanced over braces. */
56
- function useTestWorldArgs(text) {
57
- const out = [];
58
- const re = /useTestWorld\s*\(/g;
59
- let m;
60
- while ((m = re.exec(text))) {
61
- let depth = 0, i = m.index + m[0].length, start = i;
62
- for (; i < text.length; i += 1) {
63
- const c = text[i];
64
- if (c === '(' || c === '{' || c === '[') depth += 1;
65
- else if (c === ')' || c === '}' || c === ']') {
66
- if (depth === 0) break;
67
- depth -= 1;
68
- }
69
- }
70
- out.push(text.slice(start, i));
71
- }
72
- return out;
73
- }
74
-
75
- /** One spec judged: {path, useTestWorld, modes[], outage, forbidden[]}; `sandbox` is a mode when the spec calls useSandbox. */
76
- export function judgeSpec(rel, text, rules) {
77
- const args = useTestWorldArgs(text);
78
- const modes = [...new Set(args.flatMap((a) => ['apps', 'modules'].filter((k) => new RegExp(String.raw`(^|[{,\s])${k}\s*:`).test(a))))];
79
- const sandbox = /useSandbox\s*\(/.test(text);
80
- if (sandbox) modes.push('sandbox');
81
- // A comment may name a forbidden word to explain its absence; only code counts.
82
- const code = text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/[^\n\r\u2028\u2029]*/gm, '$1');
83
- return { path: rel, useTestWorld: args.length > 0 || sandbox, modes, outage: rules.outage.reduce((n, needle) => n + code.split(needle).length - 1, 0),
84
- forbidden: rules.forbidden.filter((needle) => code.includes(needle)) };
85
- }
86
-
87
- /** The spec files of a world project under the app root (app-relative POSIX paths), narrowed by a --tests pattern. */
88
- export function specsOf(root, project, tests = null) {
89
- const def = WORLD_PROJECTS[project];
90
- const dir = path.join(root, def.dir);
91
- if (!fs.existsSync(dir)) return [];
92
- const narrow = tests ? new RegExp(tests) : null;
93
- return walkFiles(dir, { sorted: true, exclude: (name, full, entry) => entry.isDirectory() && name === 'node_modules' })
94
- .map((file) => posixPath(path.relative(root, file)))
95
- .filter((rel) => rel.endsWith(def.suffix) && (!narrow || narrow.test(rel)));
96
- }
97
-
98
- const RUN_COUNTS = Object.freeze(['total', 'passed', 'failed', 'skipped', 'files', 'failedFiles']);
99
- const JEST_COUNTS = Object.freeze(['numTotalTests', 'numPassedTests', 'numFailedTests', 'numPendingTests', 'numTodoTests',
100
- 'numTotalTestSuites', 'numPassedTestSuites', 'numFailedTestSuites', 'numPendingTestSuites', 'numRuntimeErrorTestSuites']);
101
-
102
- /** Reject an incomplete or inconsistent required test measurement without coercing reported counters. */
103
- export function testRunCountsError(run) {
104
- if (!run || RUN_COUNTS.some((key) => !Number.isSafeInteger(run[key]) || run[key] < 0)) return 'test counters must be non-negative safe integers';
105
- if (run.total !== run.passed + run.failed + run.skipped || run.failedFiles > run.files) return 'test counters do not describe a complete run';
106
- return null;
107
- }
108
-
109
- /** The shape checks of the report and its process: an error string or null. */
110
- const reportShapeError = (report, run) => {
111
- if (run?.error || run?.signal || !Number.isSafeInteger(run?.status) || run.status < 0) {
112
- const detail = run?.error?.message ?? run?.signal ?? `exit ${run?.status ?? 'unknown'}`;
113
- return `jest did not complete: ${detail}`;
114
- }
115
- if (!report || typeof report !== 'object' || Array.isArray(report) || !Array.isArray(report.testResults)) return 'jest wrote no valid --json report';
116
- if (JEST_COUNTS.some((key) => !Number.isSafeInteger(report[key]) || report[key] < 0)) return 'jest counters must be non-negative safe integers';
117
- if (typeof report.success !== 'boolean' || typeof report.wasInterrupted !== 'boolean') return 'jest result is missing completion flags';
118
- if (report.wasInterrupted || report.runExecError) return 'jest reports an interrupted or incomplete run';
119
- if (report.numTotalTestSuites !== report.numPassedTestSuites + report.numFailedTestSuites + report.numPendingTestSuites
120
- || report.numRuntimeErrorTestSuites > report.numFailedTestSuites || report.testResults.length !== report.numTotalTestSuites)
121
- return 'jest suite counters do not describe a complete run';
122
- return null;
123
- };
124
-
125
- /** The assertion and suite tallies of a valid report: {totals, suites}, or {error}. */
126
- const suiteShapeError = (suite) => !suite || !['passed', 'failed', 'skipped', 'focused'].includes(suite.status) || !Array.isArray(suite.assertionResults)
127
- ? 'jest wrote an invalid suite result' : null;
128
-
129
- const focusedSuiteError = (suite, report) => suite.status === 'focused'
130
- && (report.numPendingTests === 0 || !suite.assertionResults.some(a => ['pending', 'skipped', 'disabled'].includes(a?.status)))
131
- ? 'jest focused suite reports no pending assertion' : null;
132
-
133
- const tallyAssertion = (assertion, totals) => {
134
- if (assertion?.status === 'passed') totals.passed += 1;
135
- else if (assertion?.status === 'failed') totals.failed += 1;
136
- else if (['pending', 'todo', 'skipped', 'disabled'].includes(assertion?.status)) totals.skipped += 1;
137
- else return 'jest wrote an invalid assertion result';
138
- return null;
139
- };
140
-
141
- const tallySuites = (report) => {
142
- const totals = { passed: 0, failed: 0, skipped: 0 }, suites = { passed: 0, failed: 0, skipped: 0 };
143
- for (const suite of report.testResults) {
144
- const shapeError = suiteShapeError(suite);
145
- if (shapeError) return { error: shapeError };
146
- const focusedError = focusedSuiteError(suite, report);
147
- if (focusedError) return { error: focusedError };
148
- suites[suite.status === 'focused' ? 'passed' : suite.status] += 1;
149
- for (const assertion of suite.assertionResults) {
150
- const assertionError = tallyAssertion(assertion, totals);
151
- if (assertionError) return { error: assertionError };
152
- }
153
- }
154
- return { totals, suites };
155
- };
156
-
157
- /** Validate the Jest JSON and its real process result before any producer can call the measurement clean. */
158
- export function jestRunError(report, run) {
159
- const shapeError = reportShapeError(report, run);
160
- if (shapeError) return shapeError;
161
- const tally = tallySuites(report);
162
- if (tally.error) return tally.error;
163
- const { totals, suites } = tally;
164
- const countsError = testRunCountsError(reduceJest(report));
165
- if (countsError) return countsError;
166
- if (suites.passed !== report.numPassedTestSuites || suites.failed !== report.numFailedTestSuites
167
- || suites.skipped !== report.numPendingTestSuites) return 'jest suite counters disagree with its results';
168
- if (totals.passed !== report.numPassedTests || totals.failed !== report.numFailedTests
169
- || totals.skipped !== report.numPendingTests + report.numTodoTests) return 'jest assertion counters disagree with its results';
170
- const measuredFailure = report.numFailedTests > 0 || report.numFailedTestSuites > 0;
171
- // A normal failed assertion retains its typed finding; a nonzero process with a clean report never passes.
172
- if (run.status !== 0 && !measuredFailure) return `jest exited ${run.status} without a reported test failure`;
173
- if (!report.success && !measuredFailure) return 'jest reports an unsuccessful run without a reported test failure';
174
- if (report.success && measuredFailure) return 'jest success contradicts its failed tests or suites';
175
- return null;
176
- }
177
-
178
- /** Jest's totals and executed suite files; testFiles are relative to root when supplied. */
179
- export function reduceJest(report, root = null) {
180
- const suites = Array.isArray(report?.testResults) ? report.testResults : [];
181
- const failures = [];
182
- for (const file of suites) for (const a of Array.isArray(file?.assertionResults) ? file.assertionResults : []) {
183
- if (a.status === 'failed') failures.push({ file: posixPath(String(file.name ?? '')), test: a.fullName ?? a.title, message: String(a.failureMessages?.[0] ?? '').split('\n')[0].slice(0, 300) });
184
- }
185
- return { total: report?.numTotalTests ?? 0, passed: report?.numPassedTests ?? 0, failed: report?.numFailedTests ?? 0,
186
- skipped: (report?.numPendingTests ?? 0) + (report?.numTodoTests ?? 0), files: report?.numTotalTestSuites ?? 0, failedFiles: report?.numFailedTestSuites ?? 0, failures: failures.slice(0, 100),
187
- testFiles: suites.filter((suite) => (suite.assertionResults ?? []).some((assertion) => ['passed', 'failed'].includes(assertion.status)))
188
- .map((suite) => suite.name ?? suite.testFilePath).filter((file) => typeof file === 'string')
189
- .map((file) => posixPath(root ? path.relative(root, path.resolve(root, file)) : file)) };
190
- }
191
-
192
- /** Run the managed `npm run test:<project>` with jest's JSON report; {command, exit, ...reduceJest, error}. */
193
- function runWorldProject(root, project, tests = null, { npm = runNpm } = {}) {
194
- const outDir = makeTempDir('starci-test-world-run-');
195
- const outFile = path.join(outDir, 'jest.json');
196
- const args = ['run', `test:${project}`, '--', '--json', `--outputFile=${outFile}`, ...(tests ? ['--testPathPattern', tests] : [])];
197
- const run = npm(args, { cwd: root, maxBuffer: 512 * 1024 * 1024 });
198
- let report = null;
199
- try { report = JSON.parse(fs.readFileSync(outFile, 'utf8')); } catch { report = null; }
200
- try { fs.rmSync(outFile, { force: true }); fs.rmdirSync(outDir); } catch { /* a temp file of this run */ }
201
- return { command: `npm ${args.filter((a) => !a.startsWith('--outputFile=')).join(' ')}`, exit: run.status ?? null,
202
- ...(report ? reduceJest(report, root) : { total: 0, passed: 0, failed: 0, skipped: 0, files: 0, failedFiles: 0, failures: [] }),
203
- error: jestRunError(report, run) };
204
- }
205
-
206
- /** The findings of a summary (harness, specs and run), each {rule, path, message}. */
207
- export function testWorldFindings(summary) {
208
- const out = [];
209
- const h = summary.harness ?? {};
210
- if (!h.preset) out.push({ rule: 'preset-missing', path: h.jestConfig, message: 'the be jest config is not @starci/jest-preset starciJestConfig(): its world projects do not run on the world runner' });
211
- if (!h.declaration || !h.defineTestWorld) out.push({ rule: 'declaration-missing', path: DECLARATION, message: 'no world declaration calling defineTestWorld' });
212
- if (!(summary.specs ?? []).length) out.push({ rule: 'no-specs', path: WORLD_PROJECTS[summary.project]?.dir ?? null, message: `no ${summary.project} spec was selected` });
213
- const mode = WORLD_PROJECTS[summary.project]?.mode;
214
- for (const spec of summary.specs ?? []) {
215
- const form = mode === 'sandbox' ? 'useSandbox(...)' : `useTestWorld({ ${mode} })`;
216
- if (!spec.useTestWorld) out.push({ rule: 'use-test-world-missing', path: spec.path, message: 'the spec never calls useTestWorld or useSandbox: its world is hand-rolled' });
217
- else if (mode && !spec.modes.includes(mode)) out.push({ rule: 'layer-mode', path: spec.path, message: `a ${summary.project} spec calls ${form}; it uses ${spec.modes.join(', ') || 'neither apps nor modules'}` });
218
- for (const needle of spec.forbidden) out.push({ rule: 'hand-rolled', path: spec.path, message: `the spec uses ${needle}: infrastructure belongs to @starci/test-world` });
219
- }
220
- return out;
221
- }
222
-
223
- function buildTestWorldRun({ root, project, tests = null, rules = testWorldRules(), npm = runNpm, run = true }) {
224
- if (!WORLD_PROJECTS[project]) throw new Error(`--project must be one of ${Object.keys(WORLD_PROJECTS).join(', ')}; ${USAGE}`);
225
- const abs = path.resolve(root);
226
- const specs = specsOf(abs, project, tests).map((rel) => judgeSpec(rel, fs.readFileSync(path.join(abs, rel), 'utf8'), rules));
227
- const summary = { schema: TEST_WORLD_RUN_SCHEMA, at: new Date().toISOString(), root: posixPath(abs), project, tests, harness: harnessOf(abs), specs,
228
- outageCalls: specs.reduce((n, s) => n + s.outage, 0), run: null, findings: [], exit: 2 };
229
- summary.findings = testWorldFindings(summary);
230
- summary.run = run ? runWorldProject(abs, project, tests, { npm }) : null;
231
- const red = !summary.run || summary.run.error || summary.run.exit !== 0 || summary.run.failed > 0 || summary.run.skipped > 0 || summary.run.total === 0;
232
- if (summary.run?.error) summary.exit = 2;
233
- else summary.exit = summary.findings.length || red ? 1 : 0;
234
- return summary;
235
- }
236
-
237
- function parseTestWorldArgs(argv) {
238
- const opts = { root: null, project: null, tests: null, out: null, ...valueFlags(argv, ['--root', '--project', '--tests', '--out'], USAGE) };
239
- if (!opts.project) throw new Error(`--project is required; ${USAGE}`);
240
- return opts;
241
- }
242
-
243
- if (isMain(import.meta.url)) {
244
- try {
245
- const opts = parseTestWorldArgs(process.argv.slice(2));
246
- const summary = buildTestWorldRun({ root: opts.root ?? process.cwd(), project: opts.project, tests: opts.tests });
247
- process.exitCode = emitGateSummary(summary, opts.out);
248
- } catch (error) {
249
- process.stderr.write(`test-world-run: ${error.message}\n`);
250
- process.exitCode = 2;
251
- }
252
- }