|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | +// |
| 3 | +// Evidence path resolution for the liveness gate. |
| 4 | +// |
| 5 | +// WHY THIS EXISTS. The gate's stale-evidence check used to be one line: |
| 6 | +// |
| 7 | +// const file = String(led.evidence).split(':')[0]; |
| 8 | +// if (/\//.test(file) && !existsSync(join(repoRoot, file))) → flag |
| 9 | +// |
| 10 | +// i.e. it assumed every `evidence` string is exactly `path/to/file.ts:123`. |
| 11 | +// Almost none are. Real entries look like: |
| 12 | +// |
| 13 | +// "packages/spec/src/stack.zod.ts (mergeActionsIntoObjects stable-sorts …)" |
| 14 | +// "packages/objectql/src/validation/rule-validator.ts (UPDATE strip); packages/…" |
| 15 | +// "objectui: packages/app-shell/src/views/RecordDetailView.tsx + utils/…" |
| 16 | +// |
| 17 | +// Taking everything before the first colon turns the prose into the "filename", |
| 18 | +// which never exists — so the check flagged 48 of 227 entries, and **every one |
| 19 | +// of the 48 was either a parse artefact or a deliberate cross-repo pointer**. A |
| 20 | +// warning list that is permanently non-empty and ~100% false is a warning |
| 21 | +// nobody reads: the one genuinely rotted pointer in that list |
| 22 | +// (`object.enable.clone` → a file that moved repos) sat there unnoticed. |
| 23 | +// |
| 24 | +// So: extract path-shaped tokens properly, and honour the cross-repo attribution |
| 25 | +// the entries already write ("objectui: …"). What's left is signal. |
| 26 | + |
| 27 | +/** Top-level directories of THIS repo — the anchor for a repo-relative path claim. */ |
| 28 | +export const REPO_ROOTS = ['apps', 'content', 'docker', 'docs', 'examples', 'packages', 'scripts', 'skills']; |
| 29 | + |
| 30 | +/** |
| 31 | + * Realm markers an evidence string may use to attribute a path to another repo. |
| 32 | + * `objectui` is the renderer repo; `cloud` is the closed EE runtime. `framework` |
| 33 | + * switches back explicitly. These are already the house convention in prose — |
| 34 | + * this makes them machine-read instead of decorative. |
| 35 | + */ |
| 36 | +export const FOREIGN_REALMS = ['objectui', 'cloud', 'ee']; |
| 37 | +export const LOCAL_REALM = 'framework'; |
| 38 | + |
| 39 | +/** |
| 40 | + * Paths that are repo-rooted in shape but never present in the OPEN edition. |
| 41 | + * `@objectstack/service-ai` is the closed cloud runtime — `packages/services/` |
| 42 | + * here has every sibling service EXCEPT service-ai. Entries cite it because that |
| 43 | + * runtime is what consumes the property (see the `_note` in action.json). |
| 44 | + */ |
| 45 | +export const FOREIGN_PATH_PREFIXES = ['packages/services/service-ai/']; |
| 46 | + |
| 47 | +const PATH_RE = new RegExp(`^(?:${REPO_ROOTS.join('|')})/[\\w.@-]+(?:/[\\w.@-]+)*\\.[a-zA-Z]{1,5}$`); |
| 48 | + |
| 49 | +export interface EvidenceScan { |
| 50 | + /** Repo-rooted paths attributed to THIS repo — these must resolve. */ |
| 51 | + local: string[]; |
| 52 | + /** Paths attributed to another repo (realm marker or foreign prefix) — not resolved here. */ |
| 53 | + foreign: string[]; |
| 54 | +} |
| 55 | + |
| 56 | +/** |
| 57 | + * Strip surrounding punctuation and any `:123` / `:12-34` line suffix from a |
| 58 | + * token. The trailing class includes `:` so a realm marker written `objectui:` |
| 59 | + * reduces to `objectui`; a line suffix (`file.ts:150`) ends in a digit, so it |
| 60 | + * survives that pass and is removed by the line-number rule after it. |
| 61 | + */ |
| 62 | +function bareToken(raw: string): string { |
| 63 | + return raw |
| 64 | + .replace(/^[([{<"'`,;]+/, '') |
| 65 | + .replace(/[)\]}>"'`,;.:]+$/, '') |
| 66 | + .replace(/:\d+(?:-\d+)?$/, ''); |
| 67 | +} |
| 68 | + |
| 69 | +/** |
| 70 | + * Split an evidence string into local vs foreign path claims. |
| 71 | + * |
| 72 | + * A realm marker (`objectui:`, `(objectui`, `cloud:`) attributes the paths that |
| 73 | + * FOLLOW it, and its scope ends at the next clause boundary (`;` or a closing |
| 74 | + * paren) — so `"objectui X gates … (plugin-audit, packages/plugins/…/y.ts) …"` |
| 75 | + * still resolves the framework path in the trailing clause. Anything that is not |
| 76 | + * repo-rooted (`app-shell/MetadataProvider.tsx`, `action-button/-group`) is |
| 77 | + * prose and is neither resolved nor reported. |
| 78 | + */ |
| 79 | +export function scanEvidence(evidence: string): EvidenceScan { |
| 80 | + const local: string[] = []; |
| 81 | + const foreign: string[] = []; |
| 82 | + let realm = LOCAL_REALM; |
| 83 | + |
| 84 | + for (const raw of String(evidence).split(/\s+/)) { |
| 85 | + const token = bareToken(raw); |
| 86 | + const asRealm = token.toLowerCase(); |
| 87 | + |
| 88 | + if (FOREIGN_REALMS.includes(asRealm)) { realm = asRealm; continue; } |
| 89 | + if (asRealm === LOCAL_REALM) { realm = LOCAL_REALM; continue; } |
| 90 | + |
| 91 | + if (PATH_RE.test(token)) { |
| 92 | + const isForeignPath = FOREIGN_PATH_PREFIXES.some((p) => token.startsWith(p)); |
| 93 | + if (realm !== LOCAL_REALM || isForeignPath) foreign.push(token); |
| 94 | + else local.push(token); |
| 95 | + } |
| 96 | + |
| 97 | + // A clause boundary ends a realm's scope; the path above is classified first. |
| 98 | + if (/[;)]/.test(raw)) realm = LOCAL_REALM; |
| 99 | + } |
| 100 | + |
| 101 | + return { local: dedupe(local), foreign: dedupe(foreign) }; |
| 102 | +} |
| 103 | + |
| 104 | +function dedupe(xs: string[]): string[] { |
| 105 | + return [...new Set(xs)]; |
| 106 | +} |
| 107 | + |
| 108 | +export interface EvidenceCheck extends EvidenceScan { |
| 109 | + /** Local paths that do not exist — genuinely rotted pointers. */ |
| 110 | + missing: string[]; |
| 111 | +} |
| 112 | + |
| 113 | +/** Scan an evidence string and resolve its local paths against the filesystem. */ |
| 114 | +export function checkEvidence(evidence: unknown, exists: (path: string) => boolean): EvidenceCheck { |
| 115 | + if (typeof evidence !== 'string') return { local: [], foreign: [], missing: [] }; |
| 116 | + const scan = scanEvidence(evidence); |
| 117 | + return { ...scan, missing: scan.local.filter((p) => !exists(p)) }; |
| 118 | +} |
0 commit comments