@encore-os/eos-spec 0.3.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 (256) hide show
  1. package/LICENSE +17 -0
  2. package/bin/check-dist-fresh.js +51 -0
  3. package/bin/eos-spec.js +7 -0
  4. package/dist/commands/ac-backfill.d.ts +31 -0
  5. package/dist/commands/ac-backfill.js +198 -0
  6. package/dist/commands/ac-backfill.js.map +1 -0
  7. package/dist/commands/archive.d.ts +2 -0
  8. package/dist/commands/archive.js +101 -0
  9. package/dist/commands/archive.js.map +1 -0
  10. package/dist/commands/audit.d.ts +30 -0
  11. package/dist/commands/audit.js +107 -0
  12. package/dist/commands/audit.js.map +1 -0
  13. package/dist/commands/backfill.d.ts +41 -0
  14. package/dist/commands/backfill.js +239 -0
  15. package/dist/commands/backfill.js.map +1 -0
  16. package/dist/commands/checkArtifacts.d.ts +2 -0
  17. package/dist/commands/checkArtifacts.js +48 -0
  18. package/dist/commands/checkArtifacts.js.map +1 -0
  19. package/dist/commands/checkPlacement.d.ts +2 -0
  20. package/dist/commands/checkPlacement.js +31 -0
  21. package/dist/commands/checkPlacement.js.map +1 -0
  22. package/dist/commands/claim.d.ts +4 -0
  23. package/dist/commands/claim.js +46 -0
  24. package/dist/commands/claim.js.map +1 -0
  25. package/dist/commands/complete.d.ts +2 -0
  26. package/dist/commands/complete.js +138 -0
  27. package/dist/commands/complete.js.map +1 -0
  28. package/dist/commands/conformance.d.ts +23 -0
  29. package/dist/commands/conformance.js +124 -0
  30. package/dist/commands/conformance.js.map +1 -0
  31. package/dist/commands/create.d.ts +6 -0
  32. package/dist/commands/create.js +277 -0
  33. package/dist/commands/create.js.map +1 -0
  34. package/dist/commands/deferred.d.ts +2 -0
  35. package/dist/commands/deferred.js +85 -0
  36. package/dist/commands/deferred.js.map +1 -0
  37. package/dist/commands/delta.d.ts +17 -0
  38. package/dist/commands/delta.js +292 -0
  39. package/dist/commands/delta.js.map +1 -0
  40. package/dist/commands/freshness.d.ts +13 -0
  41. package/dist/commands/freshness.js +72 -0
  42. package/dist/commands/freshness.js.map +1 -0
  43. package/dist/commands/funnel.d.ts +2 -0
  44. package/dist/commands/funnel.js +61 -0
  45. package/dist/commands/funnel.js.map +1 -0
  46. package/dist/commands/gen-tests.d.ts +2 -0
  47. package/dist/commands/gen-tests.js +53 -0
  48. package/dist/commands/gen-tests.js.map +1 -0
  49. package/dist/commands/init.d.ts +2 -0
  50. package/dist/commands/init.js +64 -0
  51. package/dist/commands/init.js.map +1 -0
  52. package/dist/commands/list.d.ts +9 -0
  53. package/dist/commands/list.js +61 -0
  54. package/dist/commands/list.js.map +1 -0
  55. package/dist/commands/metrics.d.ts +2 -0
  56. package/dist/commands/metrics.js +66 -0
  57. package/dist/commands/metrics.js.map +1 -0
  58. package/dist/commands/next.d.ts +48 -0
  59. package/dist/commands/next.js +344 -0
  60. package/dist/commands/next.js.map +1 -0
  61. package/dist/commands/precedent.d.ts +66 -0
  62. package/dist/commands/precedent.js +343 -0
  63. package/dist/commands/precedent.js.map +1 -0
  64. package/dist/commands/ready.d.ts +28 -0
  65. package/dist/commands/ready.js +147 -0
  66. package/dist/commands/ready.js.map +1 -0
  67. package/dist/commands/reconcile.d.ts +44 -0
  68. package/dist/commands/reconcile.js +187 -0
  69. package/dist/commands/reconcile.js.map +1 -0
  70. package/dist/commands/rollup.d.ts +2 -0
  71. package/dist/commands/rollup.js +43 -0
  72. package/dist/commands/rollup.js.map +1 -0
  73. package/dist/commands/scorecard.d.ts +11 -0
  74. package/dist/commands/scorecard.js +55 -0
  75. package/dist/commands/scorecard.js.map +1 -0
  76. package/dist/commands/signoff.d.ts +2 -0
  77. package/dist/commands/signoff.js +114 -0
  78. package/dist/commands/signoff.js.map +1 -0
  79. package/dist/commands/status.d.ts +2 -0
  80. package/dist/commands/status.js +100 -0
  81. package/dist/commands/status.js.map +1 -0
  82. package/dist/commands/tasks.d.ts +13 -0
  83. package/dist/commands/tasks.js +81 -0
  84. package/dist/commands/tasks.js.map +1 -0
  85. package/dist/commands/validate.d.ts +16 -0
  86. package/dist/commands/validate.js +89 -0
  87. package/dist/commands/validate.js.map +1 -0
  88. package/dist/commands/verify-acs.d.ts +15 -0
  89. package/dist/commands/verify-acs.js +196 -0
  90. package/dist/commands/verify-acs.js.map +1 -0
  91. package/dist/commands/verifyRollout.d.ts +2 -0
  92. package/dist/commands/verifyRollout.js +125 -0
  93. package/dist/commands/verifyRollout.js.map +1 -0
  94. package/dist/index.d.ts +21 -0
  95. package/dist/index.js +138 -0
  96. package/dist/index.js.map +1 -0
  97. package/dist/lib/ac-draft-prompt.d.ts +18 -0
  98. package/dist/lib/ac-draft-prompt.js +38 -0
  99. package/dist/lib/ac-draft-prompt.js.map +1 -0
  100. package/dist/lib/ac-response-parser.d.ts +18 -0
  101. package/dist/lib/ac-response-parser.js +57 -0
  102. package/dist/lib/ac-response-parser.js.map +1 -0
  103. package/dist/lib/ai-call.d.ts +13 -0
  104. package/dist/lib/ai-call.js +17 -0
  105. package/dist/lib/ai-call.js.map +1 -0
  106. package/dist/lib/archive.d.ts +21 -0
  107. package/dist/lib/archive.js +136 -0
  108. package/dist/lib/archive.js.map +1 -0
  109. package/dist/lib/artifact-integrity.d.ts +5 -0
  110. package/dist/lib/artifact-integrity.js +65 -0
  111. package/dist/lib/artifact-integrity.js.map +1 -0
  112. package/dist/lib/backfill-queue.d.ts +11 -0
  113. package/dist/lib/backfill-queue.js +14 -0
  114. package/dist/lib/backfill-queue.js.map +1 -0
  115. package/dist/lib/blockers.d.ts +66 -0
  116. package/dist/lib/blockers.js +103 -0
  117. package/dist/lib/blockers.js.map +1 -0
  118. package/dist/lib/claims.d.ts +39 -0
  119. package/dist/lib/claims.js +133 -0
  120. package/dist/lib/claims.js.map +1 -0
  121. package/dist/lib/classify-debt.d.ts +17 -0
  122. package/dist/lib/classify-debt.js +13 -0
  123. package/dist/lib/classify-debt.js.map +1 -0
  124. package/dist/lib/code-evidence.d.ts +35 -0
  125. package/dist/lib/code-evidence.js +171 -0
  126. package/dist/lib/code-evidence.js.map +1 -0
  127. package/dist/lib/coherence.d.ts +14 -0
  128. package/dist/lib/coherence.js +188 -0
  129. package/dist/lib/coherence.js.map +1 -0
  130. package/dist/lib/completion-gate.d.ts +17 -0
  131. package/dist/lib/completion-gate.js +28 -0
  132. package/dist/lib/completion-gate.js.map +1 -0
  133. package/dist/lib/compliance-signoff.d.ts +39 -0
  134. package/dist/lib/compliance-signoff.js +81 -0
  135. package/dist/lib/compliance-signoff.js.map +1 -0
  136. package/dist/lib/config.d.ts +63 -0
  137. package/dist/lib/config.js +40 -0
  138. package/dist/lib/config.js.map +1 -0
  139. package/dist/lib/cost.d.ts +12 -0
  140. package/dist/lib/cost.js +12 -0
  141. package/dist/lib/cost.js.map +1 -0
  142. package/dist/lib/debt-dashboard.d.ts +25 -0
  143. package/dist/lib/debt-dashboard.js +36 -0
  144. package/dist/lib/debt-dashboard.js.map +1 -0
  145. package/dist/lib/embedding-store.d.ts +103 -0
  146. package/dist/lib/embedding-store.js +133 -0
  147. package/dist/lib/embedding-store.js.map +1 -0
  148. package/dist/lib/embeddings-source.d.ts +36 -0
  149. package/dist/lib/embeddings-source.js +62 -0
  150. package/dist/lib/embeddings-source.js.map +1 -0
  151. package/dist/lib/enforcement-allowlist.d.ts +12 -0
  152. package/dist/lib/enforcement-allowlist.js +25 -0
  153. package/dist/lib/enforcement-allowlist.js.map +1 -0
  154. package/dist/lib/events.d.ts +47 -0
  155. package/dist/lib/events.js +75 -0
  156. package/dist/lib/events.js.map +1 -0
  157. package/dist/lib/exec.d.ts +8 -0
  158. package/dist/lib/exec.js +25 -0
  159. package/dist/lib/exec.js.map +1 -0
  160. package/dist/lib/frontmatter.d.ts +112 -0
  161. package/dist/lib/frontmatter.js +261 -0
  162. package/dist/lib/frontmatter.js.map +1 -0
  163. package/dist/lib/funnel.d.ts +22 -0
  164. package/dist/lib/funnel.js +41 -0
  165. package/dist/lib/funnel.js.map +1 -0
  166. package/dist/lib/git-mtime.d.ts +4 -0
  167. package/dist/lib/git-mtime.js +23 -0
  168. package/dist/lib/git-mtime.js.map +1 -0
  169. package/dist/lib/metrics.d.ts +42 -0
  170. package/dist/lib/metrics.js +92 -0
  171. package/dist/lib/metrics.js.map +1 -0
  172. package/dist/lib/migration-collision.d.ts +20 -0
  173. package/dist/lib/migration-collision.js +61 -0
  174. package/dist/lib/migration-collision.js.map +1 -0
  175. package/dist/lib/next-action.d.ts +44 -0
  176. package/dist/lib/next-action.js +138 -0
  177. package/dist/lib/next-action.js.map +1 -0
  178. package/dist/lib/ollama-embed.d.ts +21 -0
  179. package/dist/lib/ollama-embed.js +65 -0
  180. package/dist/lib/ollama-embed.js.map +1 -0
  181. package/dist/lib/output.d.ts +35 -0
  182. package/dist/lib/output.js +46 -0
  183. package/dist/lib/output.js.map +1 -0
  184. package/dist/lib/pipeline-config.d.ts +20 -0
  185. package/dist/lib/pipeline-config.js +208 -0
  186. package/dist/lib/pipeline-config.js.map +1 -0
  187. package/dist/lib/pipeline-map.d.ts +68 -0
  188. package/dist/lib/pipeline-map.js +256 -0
  189. package/dist/lib/pipeline-map.js.map +1 -0
  190. package/dist/lib/pipeline-schema.d.ts +48 -0
  191. package/dist/lib/pipeline-schema.js +22 -0
  192. package/dist/lib/pipeline-schema.js.map +1 -0
  193. package/dist/lib/placement.d.ts +6 -0
  194. package/dist/lib/placement.js +40 -0
  195. package/dist/lib/placement.js.map +1 -0
  196. package/dist/lib/precedent-search.d.ts +18 -0
  197. package/dist/lib/precedent-search.js +160 -0
  198. package/dist/lib/precedent-search.js.map +1 -0
  199. package/dist/lib/predicate-registry.d.ts +6 -0
  200. package/dist/lib/predicate-registry.js +40 -0
  201. package/dist/lib/predicate-registry.js.map +1 -0
  202. package/dist/lib/recall-cases.d.ts +15 -0
  203. package/dist/lib/recall-cases.js +39 -0
  204. package/dist/lib/recall-cases.js.map +1 -0
  205. package/dist/lib/repo.d.ts +20 -0
  206. package/dist/lib/repo.js +82 -0
  207. package/dist/lib/repo.js.map +1 -0
  208. package/dist/lib/requirements.d.ts +91 -0
  209. package/dist/lib/requirements.js +179 -0
  210. package/dist/lib/requirements.js.map +1 -0
  211. package/dist/lib/rollup.d.ts +24 -0
  212. package/dist/lib/rollup.js +34 -0
  213. package/dist/lib/rollup.js.map +1 -0
  214. package/dist/lib/scorecard-run.d.ts +6 -0
  215. package/dist/lib/scorecard-run.js +48 -0
  216. package/dist/lib/scorecard-run.js.map +1 -0
  217. package/dist/lib/scorecard.d.ts +69 -0
  218. package/dist/lib/scorecard.js +113 -0
  219. package/dist/lib/scorecard.js.map +1 -0
  220. package/dist/lib/semantic-search.d.ts +36 -0
  221. package/dist/lib/semantic-search.js +56 -0
  222. package/dist/lib/semantic-search.js.map +1 -0
  223. package/dist/lib/session-state.d.ts +8 -0
  224. package/dist/lib/session-state.js +25 -0
  225. package/dist/lib/session-state.js.map +1 -0
  226. package/dist/lib/spec-debt.d.ts +8 -0
  227. package/dist/lib/spec-debt.js +17 -0
  228. package/dist/lib/spec-debt.js.map +1 -0
  229. package/dist/lib/spec-pure.d.ts +59 -0
  230. package/dist/lib/spec-pure.js +215 -0
  231. package/dist/lib/spec-pure.js.map +1 -0
  232. package/dist/lib/specs.d.ts +148 -0
  233. package/dist/lib/specs.js +373 -0
  234. package/dist/lib/specs.js.map +1 -0
  235. package/dist/lib/test-generator.d.ts +5 -0
  236. package/dist/lib/test-generator.js +72 -0
  237. package/dist/lib/test-generator.js.map +1 -0
  238. package/dist/lib/user-stories.d.ts +6 -0
  239. package/dist/lib/user-stories.js +56 -0
  240. package/dist/lib/user-stories.js.map +1 -0
  241. package/dist/lib/verifies-scan.d.ts +9 -0
  242. package/dist/lib/verifies-scan.js +73 -0
  243. package/dist/lib/verifies-scan.js.map +1 -0
  244. package/dist/lib/verify-core.d.ts +23 -0
  245. package/dist/lib/verify-core.js +48 -0
  246. package/dist/lib/verify-core.js.map +1 -0
  247. package/dist/lib/with-event-log.d.ts +12 -0
  248. package/dist/lib/with-event-log.js +65 -0
  249. package/dist/lib/with-event-log.js.map +1 -0
  250. package/dist/lib/wizard-specs.d.ts +17 -0
  251. package/dist/lib/wizard-specs.js +34 -0
  252. package/dist/lib/wizard-specs.js.map +1 -0
  253. package/dist/pure.d.ts +19 -0
  254. package/dist/pure.js +31 -0
  255. package/dist/pure.js.map +1 -0
  256. package/package.json +69 -0
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Committed dense-embedding index for semantic precedent search. The matrix is a
3
+ * row-major Float32 binary aligned with `manifest.entries`; both are built offline
4
+ * by `scripts/build-spec-embeddings.ts` (Ollama) and committed so CI/runtime can
5
+ * rank without an embedding service. Mirrors the repo's PF-75 embedding-store
6
+ * pattern, reimplemented here because eos-spec cannot import from `src/`.
7
+ */
8
+ export interface EmbeddingEntry {
9
+ id: string;
10
+ file: string;
11
+ title: string;
12
+ }
13
+ export interface EmbeddingManifest {
14
+ /** Embedding model id (e.g. "qwen3-embedding:0.6b"). */
15
+ model: string;
16
+ /** Vector dimensionality (rows in the .bin are this long). */
17
+ dims: number;
18
+ /** Query instruction prefix — documents are embedded WITHOUT it, queries WITH it. */
19
+ instruction: string;
20
+ /** SHA-256 of the concatenated embedded document texts — corpus drift detector. */
21
+ surfaceHash: string;
22
+ /** ISO build date (informational; not load-bearing). */
23
+ built: string;
24
+ /** Per-row metadata, aligned with the matrix rows. */
25
+ entries: EmbeddingEntry[];
26
+ }
27
+ export interface EmbeddingIndex {
28
+ manifest: EmbeddingManifest;
29
+ /** Unit vectors, row-aligned with `manifest.entries`. */
30
+ matrix: Float32Array[];
31
+ }
32
+ /** Legacy data-dir default — the encoreos fork layout (`tools/eos-spec/data`).
33
+ * Configurable per-consumer via `specs/config.yaml` `embeddings.data_dir`, read
34
+ * through the {@link EmbeddingsSource} seam (embeddings-source.ts). The DATA_DIR
35
+ * must never be assumed package-relative — that is the "resolves to the wrong
36
+ * root" bug class the npm-pack smoke guards against (epic #300, Wave-1 C1). */
37
+ export declare const DEFAULT_DATA_DIR = "tools/eos-spec/data";
38
+ /** Legacy spec-index path default (relative to repo root). */
39
+ export declare const DEFAULT_SPEC_INDEX_PATH = "specs/spec-index.json";
40
+ export declare const MANIFEST_FILE = "spec-embeddings.manifest.json";
41
+ export declare const MATRIX_FILE = "spec-embeddings.bin";
42
+ export declare const RECALL_VECTORS_FILE = "recall-query-vectors.json";
43
+ export declare function dataPath(repoRoot: string, file: string, dataDir?: string): string;
44
+ /** Unit-length copy of a vector (zero vector returned unchanged). */
45
+ export declare function normalize(v: Float32Array): Float32Array;
46
+ /** Dot product (== cosine similarity for unit vectors). */
47
+ export declare function dot(a: Float32Array, b: Float32Array): number;
48
+ /** Serialize rows to a little-endian Float32 row-major Buffer (build-time). */
49
+ export declare function serializeMatrix(rows: Float32Array[]): Buffer;
50
+ /** Parse a row-major Float32 buffer into per-row vectors. */
51
+ export declare function parseMatrix(buf: ArrayBuffer, dims: number): Float32Array[];
52
+ /** Load the committed embedding index from `repoRoot`/`dataDir`, or null when absent. */
53
+ export declare function loadEmbeddingIndex(repoRoot: string, dataDir?: string): EmbeddingIndex | null;
54
+ /** One ranked semantic hit. */
55
+ export interface SemanticHit {
56
+ id: string;
57
+ file: string;
58
+ title: string;
59
+ score: number;
60
+ }
61
+ /** Rank every index entry by cosine to a (normalized) query vector, descending. */
62
+ export declare function cosineRank(queryVec: Float32Array, index: EmbeddingIndex, limit?: number): SemanticHit[];
63
+ /** Cap on a single document's embedded text — must match the build script. */
64
+ export declare const DOC_CAP = 1200;
65
+ /** Minimal spec-index row needed to recompute the embedding surface hash. */
66
+ export interface SurfaceSpec {
67
+ id?: string | null;
68
+ title?: string | null;
69
+ file?: string | null;
70
+ status?: string | null;
71
+ summary?: string | null;
72
+ }
73
+ /** A spec is embedded only when it is non-archived and has a file. */
74
+ export declare function isEmbeddableSpec(s: SurfaceSpec | null): s is SurfaceSpec;
75
+ /** The document text embedded for one spec (title + summary, capped). */
76
+ export declare function embedDocText(s: SurfaceSpec): string;
77
+ /** SHA-256 of the concatenated embedded document texts — the corpus surface. */
78
+ export declare function computeSurfaceHash(specs: (SurfaceSpec | null)[]): string;
79
+ /** Outcome of a freshness check between the live corpus and committed vectors. */
80
+ export interface FreshnessResult {
81
+ fresh: boolean;
82
+ /** Hash recomputed from the current specs/spec-index.json. */
83
+ currentHash: string;
84
+ /** Hash recorded in the committed manifest (null when the manifest is absent). */
85
+ committedHash: string | null;
86
+ /** Why a check could not be performed, when applicable. */
87
+ reason?: 'manifest-missing' | 'index-missing';
88
+ }
89
+ /**
90
+ * Compare the live corpus surface against the committed embedding manifest.
91
+ *
92
+ * Reads the generated (gitignored) spec-index.json, recomputes the surface hash,
93
+ * and compares it to the committed manifest hash. `fresh` is true only when both
94
+ * are present and equal. A missing manifest or index is reported as NOT fresh with
95
+ * a `reason` so callers can produce an actionable message. The data dir +
96
+ * spec-index path are configurable per-consumer via the {@link EmbeddingsSource}
97
+ * seam (embeddings-source.ts → `specs/config.yaml`); they default to the legacy
98
+ * fork layout when omitted.
99
+ */
100
+ export declare function checkEmbeddingFreshness(repoRoot: string, opts?: {
101
+ dataDir?: string;
102
+ specIndexPath?: string;
103
+ }): FreshnessResult;
@@ -0,0 +1,133 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ /** Legacy data-dir default — the encoreos fork layout (`tools/eos-spec/data`).
5
+ * Configurable per-consumer via `specs/config.yaml` `embeddings.data_dir`, read
6
+ * through the {@link EmbeddingsSource} seam (embeddings-source.ts). The DATA_DIR
7
+ * must never be assumed package-relative — that is the "resolves to the wrong
8
+ * root" bug class the npm-pack smoke guards against (epic #300, Wave-1 C1). */
9
+ export const DEFAULT_DATA_DIR = 'tools/eos-spec/data';
10
+ /** Legacy spec-index path default (relative to repo root). */
11
+ export const DEFAULT_SPEC_INDEX_PATH = 'specs/spec-index.json';
12
+ export const MANIFEST_FILE = 'spec-embeddings.manifest.json';
13
+ export const MATRIX_FILE = 'spec-embeddings.bin';
14
+ export const RECALL_VECTORS_FILE = 'recall-query-vectors.json';
15
+ export function dataPath(repoRoot, file, dataDir = DEFAULT_DATA_DIR) {
16
+ return join(repoRoot, dataDir, file);
17
+ }
18
+ /** Unit-length copy of a vector (zero vector returned unchanged). */
19
+ export function normalize(v) {
20
+ let sum = 0;
21
+ for (const x of v)
22
+ sum += x * x;
23
+ const mag = Math.sqrt(sum);
24
+ if (mag === 0)
25
+ return v.slice();
26
+ const out = new Float32Array(v.length);
27
+ for (let i = 0; i < v.length; i++)
28
+ out[i] = v[i] / mag;
29
+ return out;
30
+ }
31
+ /** Dot product (== cosine similarity for unit vectors). */
32
+ export function dot(a, b) {
33
+ let s = 0;
34
+ const n = Math.min(a.length, b.length);
35
+ for (let i = 0; i < n; i++)
36
+ s += a[i] * b[i];
37
+ return s;
38
+ }
39
+ /** Serialize rows to a little-endian Float32 row-major Buffer (build-time). */
40
+ export function serializeMatrix(rows) {
41
+ const dims = rows.length > 0 ? rows[0].length : 0;
42
+ const flat = new Float32Array(rows.length * dims);
43
+ for (let i = 0; i < rows.length; i++)
44
+ flat.set(rows[i], i * dims);
45
+ return Buffer.from(flat.buffer);
46
+ }
47
+ /** Parse a row-major Float32 buffer into per-row vectors. */
48
+ export function parseMatrix(buf, dims) {
49
+ if (dims <= 0)
50
+ return [];
51
+ const flat = new Float32Array(buf);
52
+ const rows = [];
53
+ for (let i = 0; i < flat.length; i += dims)
54
+ rows.push(flat.slice(i, i + dims));
55
+ return rows;
56
+ }
57
+ /** Load the committed embedding index from `repoRoot`/`dataDir`, or null when absent. */
58
+ export function loadEmbeddingIndex(repoRoot, dataDir = DEFAULT_DATA_DIR) {
59
+ const manifestPath = dataPath(repoRoot, MANIFEST_FILE, dataDir);
60
+ const matrixPath = dataPath(repoRoot, MATRIX_FILE, dataDir);
61
+ if (!existsSync(manifestPath) || !existsSync(matrixPath))
62
+ return null;
63
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
64
+ const file = readFileSync(matrixPath);
65
+ // Slice to the exact backing region — readFileSync may share a pooled buffer.
66
+ const ab = file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength);
67
+ const matrix = parseMatrix(ab, manifest.dims);
68
+ return { manifest, matrix };
69
+ }
70
+ /** Rank every index entry by cosine to a (normalized) query vector, descending. */
71
+ export function cosineRank(queryVec, index, limit) {
72
+ const hits = index.manifest.entries.map((e, i) => ({
73
+ id: e.id,
74
+ file: e.file,
75
+ title: e.title,
76
+ score: dot(queryVec, index.matrix[i] ?? new Float32Array()),
77
+ }));
78
+ hits.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
79
+ return typeof limit === 'number' ? hits.slice(0, limit) : hits;
80
+ }
81
+ // --- Corpus surface-hash (freshness gate) -----------------------------------
82
+ // The committed embedding manifest records the SHA-256 of the embedded document
83
+ // texts. These helpers recompute that hash from the *current* spec-index.json so
84
+ // `eos-spec freshness` can detect when the corpus has drifted away from the
85
+ // committed vectors. They MUST mirror the offline embedding build exactly (same
86
+ // archived filter, same docText shape, same join) or the hash will never match a
87
+ // freshly-built index. Ported from encoreos (the engine was "encoreos minus freshness").
88
+ /** Cap on a single document's embedded text — must match the build script. */
89
+ export const DOC_CAP = 1200;
90
+ /** A spec is embedded only when it is non-archived and has a file. */
91
+ export function isEmbeddableSpec(s) {
92
+ if (!s?.file)
93
+ return false;
94
+ return !(s.status === 'archived' || (s.file ?? '').includes('/archive/'));
95
+ }
96
+ /** The document text embedded for one spec (title + summary, capped). */
97
+ export function embedDocText(s) {
98
+ return `${s.title ?? ''}\n${s.summary ?? ''}`.slice(0, DOC_CAP);
99
+ }
100
+ /** SHA-256 of the concatenated embedded document texts — the corpus surface. */
101
+ export function computeSurfaceHash(specs) {
102
+ const docTexts = specs.filter(isEmbeddableSpec).map(embedDocText);
103
+ return createHash('sha256').update(docTexts.join(' ')).digest('hex');
104
+ }
105
+ /**
106
+ * Compare the live corpus surface against the committed embedding manifest.
107
+ *
108
+ * Reads the generated (gitignored) spec-index.json, recomputes the surface hash,
109
+ * and compares it to the committed manifest hash. `fresh` is true only when both
110
+ * are present and equal. A missing manifest or index is reported as NOT fresh with
111
+ * a `reason` so callers can produce an actionable message. The data dir +
112
+ * spec-index path are configurable per-consumer via the {@link EmbeddingsSource}
113
+ * seam (embeddings-source.ts → `specs/config.yaml`); they default to the legacy
114
+ * fork layout when omitted.
115
+ */
116
+ export function checkEmbeddingFreshness(repoRoot, opts = {}) {
117
+ const dataDir = opts.dataDir ?? DEFAULT_DATA_DIR;
118
+ const manifestPath = dataPath(repoRoot, MANIFEST_FILE, dataDir);
119
+ const committedHash = existsSync(manifestPath)
120
+ ? JSON.parse(readFileSync(manifestPath, 'utf8')).surfaceHash
121
+ : null;
122
+ const indexPath = join(repoRoot, opts.specIndexPath ?? DEFAULT_SPEC_INDEX_PATH);
123
+ if (!existsSync(indexPath)) {
124
+ return { fresh: false, currentHash: '', committedHash, reason: 'index-missing' };
125
+ }
126
+ const index = JSON.parse(readFileSync(indexPath, 'utf8'));
127
+ const currentHash = computeSurfaceHash(Array.isArray(index.specs) ? index.specs : []);
128
+ if (committedHash === null) {
129
+ return { fresh: false, currentHash, committedHash, reason: 'manifest-missing' };
130
+ }
131
+ return { fresh: currentHash === committedHash, currentHash, committedHash };
132
+ }
133
+ //# sourceMappingURL=embedding-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embedding-store.js","sourceRoot":"","sources":["../../src/lib/embedding-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import { type EmbeddingIndex, type FreshnessResult } from './embedding-store.js';
2
+ /** Where a consumer's committed embedding index + freshness corpus live. */
3
+ export interface EmbeddingsPaths {
4
+ /** Index dir (manifest + matrix + recall vectors), relative to repo root. */
5
+ dataDir: string;
6
+ /** Generated spec-index.json (freshness corpus), relative to repo root. */
7
+ specIndexPath: string;
8
+ }
9
+ /**
10
+ * The injectable embedding backend. Semantic precedent search and the freshness
11
+ * gate consume this, never the raw filesystem paths — so a non-filesystem source
12
+ * (pgvector, tenant-scoped) is a drop-in.
13
+ */
14
+ export interface EmbeddingsSource {
15
+ /** Load the committed dense index, or null when absent. */
16
+ loadIndex(): EmbeddingIndex | null;
17
+ /** Compare the live corpus surface against the committed manifest. */
18
+ checkFreshness(): FreshnessResult;
19
+ }
20
+ /**
21
+ * Resolve the embedding paths for `repoRoot` from `specs/config.yaml`
22
+ * (`embeddings.*`), falling back to the legacy fork-layout defaults when the
23
+ * config is absent, unreadable, or omits the `embeddings` block — so existing
24
+ * consumers behave exactly as before.
25
+ */
26
+ export declare function resolveEmbeddingsPaths(repoRoot: string, configOverride?: string): EmbeddingsPaths;
27
+ /** Filesystem-backed source: reads committed vectors + spec-index from disk. */
28
+ export declare class FilesystemEmbeddingsSource implements EmbeddingsSource {
29
+ private readonly repoRoot;
30
+ private readonly paths;
31
+ constructor(repoRoot: string, paths: EmbeddingsPaths);
32
+ loadIndex(): EmbeddingIndex | null;
33
+ checkFreshness(): FreshnessResult;
34
+ }
35
+ /** The default source for a repo: filesystem-backed, paths resolved from config. */
36
+ export declare function embeddingsSourceFromConfig(repoRoot: string, configOverride?: string): EmbeddingsSource;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The EmbeddingsSource provider seam (epic #300, Wave-1 C1).
3
+ *
4
+ * The engine carries NO corpus data — the embedding index location is injected
5
+ * per-consumer via `specs/config.yaml` (`embeddings.data_dir` /
6
+ * `embeddings.spec_index_path`), resolved here. Today the only implementation is
7
+ * filesystem-backed; the interface is the seam a future pgvector / tenant-scoped
8
+ * source (cloud, per-tenant) drops into without touching `semantic-search` or the
9
+ * `freshness` command.
10
+ *
11
+ * Keeping the data dir behind this seam — rather than a package-relative constant —
12
+ * is what lets the published tarball ship no `data/` and a consumer point at its
13
+ * own committed vectors, and it kills the "DATA_DIR resolves to the wrong root"
14
+ * bug class the npm-pack smoke guards against.
15
+ */
16
+ import { loadConfig } from './config.js';
17
+ import { checkEmbeddingFreshness, DEFAULT_DATA_DIR, DEFAULT_SPEC_INDEX_PATH, loadEmbeddingIndex, } from './embedding-store.js';
18
+ /**
19
+ * Resolve the embedding paths for `repoRoot` from `specs/config.yaml`
20
+ * (`embeddings.*`), falling back to the legacy fork-layout defaults when the
21
+ * config is absent, unreadable, or omits the `embeddings` block — so existing
22
+ * consumers behave exactly as before.
23
+ */
24
+ export function resolveEmbeddingsPaths(repoRoot, configOverride) {
25
+ let dataDir = DEFAULT_DATA_DIR;
26
+ let specIndexPath = DEFAULT_SPEC_INDEX_PATH;
27
+ try {
28
+ const cfg = loadConfig(repoRoot, configOverride);
29
+ if (cfg.embeddings?.data_dir)
30
+ dataDir = cfg.embeddings.data_dir;
31
+ if (cfg.embeddings?.spec_index_path)
32
+ specIndexPath = cfg.embeddings.spec_index_path;
33
+ }
34
+ catch {
35
+ // No / unreadable config → legacy defaults (the index simply reads as absent
36
+ // when the default dir doesn't exist, degrading semantic search to lexical).
37
+ }
38
+ return { dataDir, specIndexPath };
39
+ }
40
+ /** Filesystem-backed source: reads committed vectors + spec-index from disk. */
41
+ export class FilesystemEmbeddingsSource {
42
+ repoRoot;
43
+ paths;
44
+ constructor(repoRoot, paths) {
45
+ this.repoRoot = repoRoot;
46
+ this.paths = paths;
47
+ }
48
+ loadIndex() {
49
+ return loadEmbeddingIndex(this.repoRoot, this.paths.dataDir);
50
+ }
51
+ checkFreshness() {
52
+ return checkEmbeddingFreshness(this.repoRoot, {
53
+ dataDir: this.paths.dataDir,
54
+ specIndexPath: this.paths.specIndexPath,
55
+ });
56
+ }
57
+ }
58
+ /** The default source for a repo: filesystem-backed, paths resolved from config. */
59
+ export function embeddingsSourceFromConfig(repoRoot, configOverride) {
60
+ return new FilesystemEmbeddingsSource(repoRoot, resolveEmbeddingsPaths(repoRoot, configOverride));
61
+ }
62
+ //# sourceMappingURL=embeddings-source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embeddings-source.js","sourceRoot":"","sources":["../../src/lib/embeddings-source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EACL,uBAAuB,EACvB,gBAAgB,EAChB,uBAAuB,EAGvB,kBAAkB,GACnB,MAAM,sBAAsB,CAAC;AAsB9B;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CAAC,QAAgB,EAAE,cAAuB;IAC9E,IAAI,OAAO,GAAG,gBAAgB,CAAC;IAC/B,IAAI,aAAa,GAAG,uBAAuB,CAAC;IAC5C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,UAAU,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC;QACjD,IAAI,GAAG,CAAC,UAAU,EAAE,QAAQ;YAAE,OAAO,GAAG,GAAG,CAAC,UAAU,CAAC,QAAQ,CAAC;QAChE,IAAI,GAAG,CAAC,UAAU,EAAE,eAAe;YAAE,aAAa,GAAG,GAAG,CAAC,UAAU,CAAC,eAAe,CAAC;IACtF,CAAC;IAAC,MAAM,CAAC;QACP,6EAA6E;QAC7E,6EAA6E;IAC/E,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC;AACpC,CAAC;AAED,gFAAgF;AAChF,MAAM,OAAO,0BAA0B;IAElB;IACA;IAFnB,YACmB,QAAgB,EAChB,KAAsB;QADtB,aAAQ,GAAR,QAAQ,CAAQ;QAChB,UAAK,GAAL,KAAK,CAAiB;IACtC,CAAC;IAEJ,SAAS;QACP,OAAO,kBAAkB,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC/D,CAAC;IAED,cAAc;QACZ,OAAO,uBAAuB,CAAC,IAAI,CAAC,QAAQ,EAAE;YAC5C,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO;YAC3B,aAAa,EAAE,IAAI,CAAC,KAAK,CAAC,aAAa;SACxC,CAAC,CAAC;IACL,CAAC;CACF;AAED,oFAAoF;AACpF,MAAM,UAAU,0BAA0B,CAAC,QAAgB,EAAE,cAAuB;IAClF,OAAO,IAAI,0BAA0B,CAAC,QAAQ,EAAE,sBAAsB,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC,CAAC;AACpG,CAAC"}
@@ -0,0 +1,12 @@
1
+ export type EnforcementAllowlist = {
2
+ block: string[];
3
+ warn: string[];
4
+ };
5
+ /**
6
+ * Read scripts/specs/core-enforcement-allowlist.json — the SAME file
7
+ * validate-pipeline-status.loadEnforcementAllowlist reads — without crossing the
8
+ * scripts/ ↔ tools/eos-spec compile boundary. Returns everything-warn-only
9
+ * (`{ block: [], warn: [] }`) when the file is absent or unreadable, so a gate
10
+ * built on it never hard-refuses by accident.
11
+ */
12
+ export declare function loadEnforcementAllowlist(repoRoot: string): EnforcementAllowlist;
@@ -0,0 +1,25 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ /**
4
+ * Read scripts/specs/core-enforcement-allowlist.json — the SAME file
5
+ * validate-pipeline-status.loadEnforcementAllowlist reads — without crossing the
6
+ * scripts/ ↔ tools/eos-spec compile boundary. Returns everything-warn-only
7
+ * (`{ block: [], warn: [] }`) when the file is absent or unreadable, so a gate
8
+ * built on it never hard-refuses by accident.
9
+ */
10
+ export function loadEnforcementAllowlist(repoRoot) {
11
+ const file = join(repoRoot, 'scripts', 'specs', 'core-enforcement-allowlist.json');
12
+ if (!existsSync(file))
13
+ return { block: [], warn: [] };
14
+ try {
15
+ const parsed = JSON.parse(readFileSync(file, 'utf8'));
16
+ return {
17
+ block: Array.isArray(parsed.block) ? parsed.block : [],
18
+ warn: Array.isArray(parsed.warn) ? parsed.warn : [],
19
+ };
20
+ }
21
+ catch {
22
+ return { block: [], warn: [] };
23
+ }
24
+ }
25
+ //# sourceMappingURL=enforcement-allowlist.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enforcement-allowlist.js","sourceRoot":"","sources":["../../src/lib/enforcement-allowlist.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAIjC;;;;;;GAMG;AACH,MAAM,UAAU,wBAAwB,CAAC,QAAgB;IACvD,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,iCAAiC,CAAC,CAAC;IACnF,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;IACtD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAkC,CAAC;QACvF,OAAO;YACL,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;YACtD,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE;SACpD,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;IACjC,CAAC;AACH,CAAC"}
@@ -0,0 +1,47 @@
1
+ export type EosSpecEvent = {
2
+ ts: string;
3
+ command: string;
4
+ args?: string[];
5
+ ms: number;
6
+ ok: boolean;
7
+ /** AI-call-only fields: */
8
+ model?: string;
9
+ tokens?: {
10
+ prompt: number;
11
+ completion: number;
12
+ total: number;
13
+ };
14
+ costUsd?: number | null;
15
+ /** Optional CLI context: */
16
+ specId?: string;
17
+ core?: string;
18
+ /** Optional error message (when ok=false) */
19
+ error?: string;
20
+ /** Enforcement-gate fields (harness-spec-awareness design §4.6) — set when a
21
+ * gate fires; consumed later by gate-precision reporting. Additive, optional. */
22
+ gate?: string;
23
+ verdict?: 'pass' | 'warn' | 'block';
24
+ overridden?: boolean;
25
+ /** D1 halt-cause taxonomy: when ok=false, why. 'gate' = a governance gate
26
+ * fired (expected, autonomy working); 'crash' = unexpected tooling failure.
27
+ * Derived at read time if absent; writers may set it explicitly. */
28
+ haltCause?: 'gate' | 'crash';
29
+ /** D3 run correlation: an opaque, PHI-free id for the originating process/run.
30
+ * Set once per process (see RUN_ID below) — never a spec title/path. */
31
+ runId?: string;
32
+ };
33
+ /** Opaque, PHI-free run id for THIS process — generated once at module load.
34
+ * Prefer an externally-supplied id (a future fleet orchestrator can correlate a
35
+ * run across CLI invocations); otherwise a random UUID. Never derived from
36
+ * Date.now()/Math.random() — parallel worktree agents share the sink path and
37
+ * would collide. Must carry no PHI (a UUID, never a spec title/path). */
38
+ export declare const RUN_ID: string;
39
+ /** Relative path segments for the telemetry sink — shared so writer and reader
40
+ * can't drift (see metrics.ts). Gitignored via the root `.claude/tmp/` entry. */
41
+ export declare const EVENTS_RELATIVE_PATH: string;
42
+ export declare function logEvent(event: EosSpecEvent, opts?: {
43
+ path?: string;
44
+ }): void;
45
+ /** Convert a commander `options` object to a flat `['--key', 'value', ...]` array,
46
+ * allowlisting safe keys and redacting everything else (treated as potential secrets). */
47
+ export declare function redactArgs(options: Record<string, unknown>): string[];
@@ -0,0 +1,75 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { appendFileSync, mkdirSync } from 'node:fs';
4
+ import { dirname, join } from 'node:path';
5
+ /** Opaque, PHI-free run id for THIS process — generated once at module load.
6
+ * Prefer an externally-supplied id (a future fleet orchestrator can correlate a
7
+ * run across CLI invocations); otherwise a random UUID. Never derived from
8
+ * Date.now()/Math.random() — parallel worktree agents share the sink path and
9
+ * would collide. Must carry no PHI (a UUID, never a spec title/path). */
10
+ export const RUN_ID = process.env.EOS_SPEC_RUN_ID || randomUUID();
11
+ /** Relative path segments for the telemetry sink — shared so writer and reader
12
+ * can't drift (see metrics.ts). Gitignored via the root `.claude/tmp/` entry. */
13
+ export const EVENTS_RELATIVE_PATH = join('.claude', 'tmp', 'eos-spec-events.jsonl');
14
+ /** Resolve the git repo root via `git rev-parse --show-toplevel`, falling back
15
+ * to `process.cwd()` if git fails (not a repo). Never throws.
16
+ * Resolved at call time so tests that chdir() see the new cwd. */
17
+ function defaultPath() {
18
+ try {
19
+ const root = execSync('git rev-parse --show-toplevel', {
20
+ encoding: 'utf8',
21
+ stdio: ['pipe', 'pipe', 'pipe'],
22
+ }).trim();
23
+ return join(root, EVENTS_RELATIVE_PATH);
24
+ }
25
+ catch {
26
+ return join(process.cwd(), EVENTS_RELATIVE_PATH);
27
+ }
28
+ }
29
+ export function logEvent(event, opts = {}) {
30
+ try {
31
+ const path = opts.path ?? process.env.EOS_SPEC_EVENTS_PATH ?? defaultPath();
32
+ mkdirSync(dirname(path), { recursive: true });
33
+ appendFileSync(path, `${JSON.stringify(event)}\n`, 'utf8');
34
+ }
35
+ catch {
36
+ // A failed telemetry write must never propagate — silently no-op.
37
+ }
38
+ }
39
+ const ALLOWED_KEYS = new Set([
40
+ 'spec',
41
+ 'core',
42
+ 'type',
43
+ 'limit',
44
+ 'dryRun',
45
+ 'write',
46
+ 'save',
47
+ 'stallDays',
48
+ 'all',
49
+ 'includeArchived',
50
+ 'since',
51
+ 'json',
52
+ 'file',
53
+ 'date',
54
+ ]);
55
+ const COMMANDER_INTERNAL = new Set(['parent', 'args', 'processedArgs', 'rawArgs', 'commands']);
56
+ /** Convert a commander `options` object to a flat `['--key', 'value', ...]` array,
57
+ * allowlisting safe keys and redacting everything else (treated as potential secrets). */
58
+ export function redactArgs(options) {
59
+ const out = [];
60
+ for (const [key, value] of Object.entries(options)) {
61
+ if (COMMANDER_INTERNAL.has(key))
62
+ continue;
63
+ if (value === undefined || value === null)
64
+ continue;
65
+ const flag = `--${key.replace(/([A-Z])/g, '-$1').toLowerCase()}`;
66
+ if (ALLOWED_KEYS.has(key)) {
67
+ out.push(flag, String(value));
68
+ }
69
+ else {
70
+ out.push(flag, '<redacted>');
71
+ }
72
+ }
73
+ return out;
74
+ }
75
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/lib/events.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AA+B1C;;;;0EAI0E;AAC1E,MAAM,CAAC,MAAM,MAAM,GAAW,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,UAAU,EAAE,CAAC;AAE1E;kFACkF;AAClF,MAAM,CAAC,MAAM,oBAAoB,GAAG,IAAI,CAAC,SAAS,EAAE,KAAK,EAAE,uBAAuB,CAAC,CAAC;AAEpF;;mEAEmE;AACnE,SAAS,WAAW;IAClB,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,QAAQ,CAAC,+BAA+B,EAAE;YACrD,QAAQ,EAAE,MAAM;YAChB,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;SAChC,CAAC,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,IAAI,CAAC,IAAI,EAAE,oBAAoB,CAAC,CAAC;IAC1C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,oBAAoB,CAAC,CAAC;IACnD,CAAC;AACH,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,KAAmB,EAAE,OAA0B,EAAE;IACxE,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,WAAW,EAAE,CAAC;QAC5E,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9C,cAAc,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC7D,CAAC;IAAC,MAAM,CAAC;QACP,kEAAkE;IACpE,CAAC;AACH,CAAC;AAED,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC;IAC3B,MAAM;IACN,MAAM;IACN,MAAM;IACN,OAAO;IACP,QAAQ;IACR,OAAO;IACP,MAAM;IACN,WAAW;IACX,KAAK;IACL,iBAAiB;IACjB,OAAO;IACP,MAAM;IACN,MAAM;IACN,MAAM;CACP,CAAC,CAAC;AACH,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,eAAe,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC,CAAC;AAE/F;2FAC2F;AAC3F,MAAM,UAAU,UAAU,CAAC,OAAgC;IACzD,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACnD,IAAI,kBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,SAAS;QAC1C,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;YAAE,SAAS;QACpD,MAAM,IAAI,GAAG,KAAK,GAAG,CAAC,OAAO,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;QACjE,IAAI,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAC1B,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QAChC,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;QAC/B,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,8 @@
1
+ export type RunResult = {
2
+ exitCode: number;
3
+ stdout: string;
4
+ stderr: string;
5
+ };
6
+ export declare function runScript(args: string[], cwd: string): RunResult;
7
+ export declare function runScriptInherit(args: string[], cwd: string): number;
8
+ export declare function tryRun(cmd: string, cwd: string): string;
@@ -0,0 +1,25 @@
1
+ import { execSync, spawnSync } from 'node:child_process';
2
+ export function runScript(args, cwd) {
3
+ const result = spawnSync(args[0], args.slice(1), {
4
+ cwd,
5
+ encoding: 'utf8',
6
+ env: process.env,
7
+ });
8
+ return {
9
+ exitCode: result.status ?? -1,
10
+ stdout: result.stdout ?? '',
11
+ stderr: result.stderr ?? '',
12
+ };
13
+ }
14
+ export function runScriptInherit(args, cwd) {
15
+ const result = spawnSync(args[0], args.slice(1), {
16
+ cwd,
17
+ stdio: 'inherit',
18
+ env: process.env,
19
+ });
20
+ return result.status ?? -1;
21
+ }
22
+ export function tryRun(cmd, cwd) {
23
+ return execSync(cmd, { cwd, encoding: 'utf8' });
24
+ }
25
+ //# sourceMappingURL=exec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exec.js","sourceRoot":"","sources":["../../src/lib/exec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAQzD,MAAM,UAAU,SAAS,CAAC,IAAc,EAAE,GAAW;IACnD,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,CAAE,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;QAChD,GAAG;QACH,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,OAAO,CAAC,GAAG;KACjB,CAAC,CAAC;IACH,OAAO;QACL,QAAQ,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;QAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,EAAE;QAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,EAAE;KAC5B,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,IAAc,EAAE,GAAW;IAC1D,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,CAAE,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;QAChD,GAAG;QACH,KAAK,EAAE,SAAS;QAChB,GAAG,EAAE,OAAO,CAAC,GAAG;KACjB,CAAC,CAAC;IACH,OAAO,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC;AAC7B,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,GAAW,EAAE,GAAW;IAC7C,OAAO,QAAQ,CAAC,GAAG,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;AAClD,CAAC"}