fix(triggers): point repository.directory at the directory the trigger packages actually live in - #15993
fix(triggers): point repository.directory at the directory the trigger packages actually live in#15993os-warren wants to merge 4 commits into
Conversation
…r packages actually live in
`@objectstack/trigger-record-change` and `@objectstack/trigger-schedule` both
declared a `repository.directory` under `packages/plugins/` that does not exist
in this repository, so the "Repository" deep link on their npm pages resolves to
a 404 instead of to their source. Both are published at 17.3.0.
@objectstack/trigger-record-change
was packages/plugins/plugin-trigger-record-change
now packages/triggers/trigger-record-change
@objectstack/trigger-schedule
was packages/plugins/plugin-trigger-schedule
now packages/triggers/trigger-schedule
Residue of two same-day renames, not one: `plugin-trigger-*` -> `trigger-*`
inside `packages/plugins/` broke the first segment, and the later promotion of
`packages/plugins/trigger-*` to a first-class `packages/triggers/` directory
broke the second. Both were pure directory renames with zero content changes, so
nothing rewrote the field.
Scope as a measured set, not a general claim: across all 81 tracked package.json
files, 57 declare `repository.directory`; 55 resolved to the manifest's own
directory before this change and 57 of 57 resolve after it. The 24 manifests
that declare no `repository.directory` at all are untouched here.
ADR-0041's `## References` section carried the same dead path as a source
pointer and is corrected with it. The rename table earlier in that record
(`plugin-trigger-record-change` -> `@objectstack/trigger-record-change`) is the
decision's own history and is deliberately left as written.
The corrected value ships inside the npm tarball, so it reaches readers only by
being published: a patch changeset accompanies the fix rather than
`skip-changeset`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
📓 Docs Drift Check
What this run could not see
Coarse fallback — 2 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
Clause-② contract review · PR #15993 (card #15478) ·
|
| ref | tracked files | package.json |
declare | EXISTS | EQUALS dirname | mismatch | undeclared |
|---|---|---|---|---|---|---|---|
merge-base aa6ba0623 |
7711 | 81 | 57 | 55/57 | 55/57 | 2 (the two named, existsAtRef=false) |
24 |
head e0fcc1455 |
7712 | 81 | 57 | 57/57 | 57/57 | 0 | 24 |
The merge-base run is the firing control: it names exactly the two card manifests and nothing else. EXISTS and EQUALS agree at both refs, so no manifest points at a directory that exists but is someone else's.
2. Is the sweep complete? — Yes. The causal account is not.
Own git grep -F at both refs, population: all tracked files (7711 / 7712), six spellings:
| spelling | merge-base | head |
|---|---|---|
packages/plugins/plugin-trigger |
4 occurrences / 3 files (ADR :194-195, 2 manifests) | 1 file — the changeset, quoting the old value |
packages/plugins/trigger- (intermediate) |
0 | 1 — the changeset's own prose (line 17), quoting history |
packages/triggers/plugin-trigger (hybrid) |
0 | 0 |
bare plugin-trigger-record-change / -schedule / @objectstack/plugin-trigger |
ADR history + packages/cli/CHANGELOG.md npm-name release rows |
same |
No third path spelling exists between the two states; the sweep has no hole. (Nit: "intermediate spelling occurs 0 times" is 1 at head — in the PR's own changeset, as a quotation. Not a pointer; noting it for precision only.)
But the two-rename story is wrong on the facts, and the dev's proof of it (ea4941ad8 is 16 files 0/0) covers only the second step. Measured:
998c4e4ea(parent):packages/plugins/plugin-trigger-*exists,directorycorrect for that path.f15d6f6f62026-06-12 07:43 — "feat(approvals): ADR-0042 SLA auto-escalation + ADR-0041 mechanical landing": 26 files, +2222/−19, 0 deletions. It copies the two packages topackages/plugins/trigger-*and leaves the old dirs in place (git ls-tree -d f15d6f6f6 packages/plugins/lists all four). The copied manifests are byte-identical to the originals except line 2,name, which was edited — so this commit touched the manifest and leftdirectorystale. Not a rename, not content-free.f8f7c549c2026-06-12 07:49 — fix: remove stale plugin-trigger-* directories left behind by the rename #1751fix-remove-stale-plugin-trigger-dirsdeletes the old dirs (a third same-day commit the account omits).ea4941ad82026-06-12 08:09 — pure renamepackages/plugins/trigger-*→packages/triggers/trigger-*, 16 files, 0/0. ✓ as stated.9a43e042f2026-07-21 (docs: update repo references for framework → objectstack rename #3380) "update repo references for framework → objectstack rename": editsrepository.urlandbugsin both these manifests; the stale"directory"line is unchanged context, one line below the editedurl, in a reviewed hunk.
So the sentence "Both were pure directory renames that changed no file contents, so nothing rewrote the field" is false under any reading, and #15991's "no content-touching diff existed for a reviewer to notice" is false twice over — one edit changed the manifest's name, another put the stale line on screen and still missed it. That is the stronger argument for a mechanical gate (a human-reviewed hunk containing the line did not catch it), so the correction costs #15991 nothing.
Required before undraft — in .changeset/trigger-packages-repository-directory-15478.md, replace the last sentence of the "residue" paragraph with something true, e.g.: "The first step (f15d6f6f6) copied the packages and edited only their name; the second (ea4941ad8) was a pure 16-file rename; a later repo-wide url rewrite (9a43e042f, #3380) edited the same repository block and left directory as it was. Nothing checked the field, because nothing reads it." The same correction belongs on #15991 as a comment (the PR body carries the claim too, but the body does not ship).
3. ADR-0041 boundary — correct
## References was a live source pointer with the dead path and is corrected. Lines 46/48/97-100 are the decision's "today → becomes" record, and the "becomes" column is a package name, not a path — it is not a pointer by construction. Nothing reads the table as data: git grep 0041 -- scripts/ .github/ → 0. scripts/check-adr-links.mjs:237 matches only [text](target) markdown links, so the code-span pointer was invisible to it by form, exactly as the dev says.
4. Gate answer — verified, and the sentence #15991 works from is right
- Population
scripts/: 336 tracked files. 245 lines containrepository; the only code-shaped use isrelease-github-releases.mjs:588(process.env.GITHUB_REPOSITORY) plus fourrepository:keys building itsowner/repoAPI context. Zero reads of a manifestrepositoryfield. Precision nit: most of the remaining hits are invariant strings inscripts/adr-anchors/*.json, not only "comment prose" — the substance holds. - Repo-wide, population 6061 tracked
.mjs/.js/.cjs/.ts/.tsx/.yml/.yaml/.shfiles:repository.directory/repository["directory"]read 0 times. "The rule never existed" is true on a named set. scripts/check-published-files.mjs: six invariants DECLARED / COMPLETE / SUFFICIENT / MINIMAL / REGISTERED / GATED, all over thefileswhitelist andexports; the wordrepositoryoccurs 0 times in the file. Contents gated, provenance metadata not gated is an accurate characterisation. No gate changed in this diff (4 files, none underscripts/). ✓
5. Changeset decision — proved on the artifact
Real npm pack on both packages at head (no dist/ built): 3-entry tarballs, and package/package.json inside each carries repository.directory. The live registry (npm view … repository) shows both at 17.3.0 with the dead packages/plugins/plugin-trigger-* path today. Both are private: false, in .changeset/config.json's fixed group, ignore: []; PR labels are documentation, dependencies, size/s, tooling — no skip-changeset. A patch changeset is the only route by which this fix reaches the 404. ✓
6. Vacuity trap — confirmed in both directions
pnpm --filter '@objectstack/__no_such_pkg_15993_review__' typecheck → exit 0, output "No projects matched the filters", no script line. The real filter echoes trigger-record-change typecheck$ tsc --noEmit && tsc --noEmit -p tsconfig.test.json and trigger-schedule typecheck$ tsc --noEmit. My first run was exit 2 on TS2307 (unbuilt @objectstack/core / @objectstack/spec/*) — a prerequisite failure, not reported as a pass; after building the 21-package dependency closure (exit 0), typecheck → exit 0, both names echoed, 0 error TS. Each exit captured after a single redirected command.
7. Declared NOT MEASURED — sanity-checked
- CI at read time (17:58Z): 33 check runs, every completed one
successorskipped(incl. Check Changeset, TypeScript Type Check, Type Check · workspace, Validate Package Dependencies); 2 still in progress (Test Core 1/6, Lint & Repo Gates). Not a pass claim. - The 24 undeclared reproduce as 10 private / 14 publishable, and
packages/triggers/trigger-api(@objectstack/trigger-api,private: false) is among the 14 — No gate readsrepository.directory, so a package move silently 404s its own npm source link — the rule never existed #15991's roster matches mine name for name. - Triage escape hatch quoted in the PR is verbatim from the Two published trigger packages declare a repository.directory that no longer exists (stale after the packages/plugins to packages/triggers move) #15478 triage comment.
- NOT MEASURED by me: the two vitest suites (101 / 57). The diff touches no source; I relied on CI's green Test Core shards rather than re-running.
8. Prose guard
Checked: "entire mismatched set" (named set: 81/57, §1), "0 elsewhere" (named set: 7712 files, §2), "never existed" (named sets: 336 + 6061 files, §4), "pnpm-workspace already lists packages/triggers/*" (pnpm-workspace.yaml:7 ✓), "exactly one path-shaped homepage/bugs/repository.url" (packages/console ✓, over all 81). The one claim that fails is the temporally-phrased one in §2 — the species the brief warned about.
🤖 Generated with Claude Code
https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
Generated by Claude Code
…and a later commit edited the adjacent `url` The shipped changeset claimed the stale `repository.directory` survived two pure renames and that "nothing rewrote the field". Measured, that is false in three ways, and the true account is a stronger argument for a mechanical check rather than a weaker one. f15d6f6 NOT a rename: a 26-file copy, +2222/-19, with all four trigger directories briefly coexisting. Each copied manifest differs from its original in exactly one line -- `name` -- so this commit edited the manifest and left `directory` stale. 290c625 deleted the originals five minutes later. ea4941a the only pure rename: 16 files, 0 insertions, 0 deletions. 9a43e04 six weeks later, rewrote `repository.url` and `bugs` in both of these manifests. The stale `directory` line sits as unchanged context ONE LINE BELOW the edited `url`. The last point is the one worth the correction. "Nothing rewrote the field" reads as the field never having been in anyone's way; in fact an author edited its immediate neighbour inside the same JSON object and the dead line was on screen in a reviewed hunk. A rule is more clearly needed when a human looked at the line and still missed it, so this strengthens the case on the gate card. No code change: the four-line metadata fix is unchanged and its verification still stands. This commit corrects prose only, in the file that ships to release notes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
…ale-repository-directory
PM verification — undrafted and armedVerified at head ⭐ The correction is better than the correction I asked forI asked for three facts. The rewritten paragraph found a fourth commit and reframes the whole account as a three-commit sequence on 2026-06-12: And it ends where the card should:
⇒ That last sentence is the gate finding stated in eight words, and it is now in the artifact that ships. ⭐ The scope paragraph is also stated as a measured set rather than a general claim — 81 tracked manifests, 57 declaring the field, 55 → 57 of 57 resolving, 24 undeclared and deliberately left to a policy question. That is the shape I have been asking for all session and it arrived without being asked for here. ⭐ And it declined a correction it could not reproduceThe reviewer's second nit attributed most That is the right response to a reviewer being partly wrong: the reviewer's substance was right (the dev's "comment prose" was inaccurate), its specifics were not, and adopting the specifics on authority would have shipped a second false sentence in the act of fixing the first. ⛔ The load-bearing claim — 0 reads of It verified the reviewer before accepting itAll three findings re-derived with its own instruments: One tooling note worth keeping
Nit 1 fixed as asked: stated as 0 at merge base, 1 at head, that one being the changeset's own quotation. Gates on the merged head: 20 path-derived + whole-tree The correction is also posted on #15991, so that card does not carry the false account into whoever takes the gate work — and it strengthens the case there: a rule that only fires on directory moves would have missed Undrafted and auto-merge armed. Generated by Claude Code |
CI red — measured NOT this PR's
The failing step, named: step 141 Merge-driver wiring gate → The control that settles ownership. The identical step fails on Where it is owned. Already filed by other seats, ahead of this comment: #15992 (p0, Why no fix is ported into this PR. The rule I work under says to port an existing fix rather than wait. It does not apply here, and the repo's own gate is why: Why no re-run. An unpinned launcher resolving to a version that rejects a flag is deterministic, not a flake. Re-running would reproduce it. The one re-run this PR is entitled to is left unspent. Local, for what it is worth as context rather than as the argument: State of this PR: contract review returned and its corrections are in ( Generated by Claude Code |
…ale-repository-directory
|
Base branch recovered — taken, as the earlier comment said it would be.
Merged Verified the merge did not disturb what was reviewed — all four of this PR's files byte-identical, with a firing control:
Auto-merge was already armed (18:23:59Z) and stays armed. Nothing here is waiting on the author. Generated by Claude Code |
Fixes #15478
@objectstack/trigger-record-changeand@objectstack/trigger-scheduleeach declared arepository.directoryunderpackages/plugins/that does not exist in this repository, so the Repository deep link on their npm pages resolves to a 404 instead of to their source. Both are published today at17.3.0.@objectstack/trigger-record-changepackages/plugins/plugin-trigger-record-changepackages/triggers/trigger-record-change@objectstack/trigger-schedulepackages/plugins/plugin-trigger-schedulepackages/triggers/trigger-scheduleThe whole class, not just the two named
The card names two packages. The census was scripted rather than eyeballed, over
git ls-filesfor every trackedpackage.json(81 files,node_modulesexcluded), comparingrepository.directoryboth against the filesystem and against the manifest's own directory — a plausible-but-wrong path fails exactly as silently as the current one, so resolving is not sufficient on its own.The script refuses to print any number unless its control fires in both directions: a directory known to exist must report EXISTS, and a deliberately impossible one must report MISSING.
package.jsonrepository.directoryThe two the card names were the entire mismatched set; the sweep found no third. Both were also the entire non-resolving set, so no manifest points at a directory that exists but belongs to someone else.
How the field went stale — corrected
f15d6f6f6+2222/-19, with all four trigger directories briefly coexisting. Each copied manifest differs from its original in exactly one line:name. So this commit edited the manifest and leftdirectorypointing at the path it was copied from.290c62514f8f7c549c, #1751).ea4941ad8packages/plugins/trigger-*topackages/triggers/trigger-*, 16 files, 0 insertions, 0 deletions. This is where the second segment went wrong.9a43e042frepository.urlandbugsin both of these manifests. The staledirectoryline sits as unchanged context one line below the editedurl, inside the samerepositoryobject, in a reviewed hunk.⭐ That last row is why the correction was worth making. "Nothing rewrote the field" reads as the field was never in anyone's way. The opposite is true in the way that matters: one commit edited its immediate neighbour inside the same JSON object, and six weeks later an author had the dead line on screen and shipped past it. A rule is more clearly needed when a human looked at the line and still missed it — so this strengthens the case on #15991 rather than softening it.
Same-migration rot in a different key — one more, corrected here
docs/adr/0041-flow-trigger-family.mdcarries the same dead path in its## Referencessection as a source pointer:That is the same defect class from the same rename — a source deep link that now 404s — so it is corrected with the manifests and named here rather than left as unreported drift.
⛔ Deliberately left as written: the same record's rename table and narrative at lines 46, 48 and 97-100. Those name the old identifiers as the decision's own history, and the table's right-hand column is a package name, not a path — not a pointer by construction. The anchored replacement could not have touched them in any case (they carry no
packages/plugins/prefix), but the distinction is the point: a References pointer is live, a decision record's account of what it renamed is not.The bounding scan is a repo-wide
git grep, so this is a measured set and not a sample. Across all tracked files at the merge base,packages/plugins/plugin-triggerappeared in exactly 4 places — the 2 manifests and these 2 ADR lines — all 4 corrected. Remainingplugin-triggermatches are the ADR history above pluspackages/cli/CHANGELOG.md, a published-version record that must not be rewritten.packages/plugins/trigger-occurs 0 times at the merge base and 1 time at head — that one occurrence is this PR's own changeset, quoting the history above. It is a quotation, not a pointer.Other path-shaped manifest keys were swept in the same pass: exactly one
homepage/bugs/repository.urlvalue repo-wide embeds a repo-relative path (packages/console, which exists and is unrelated to this migration), andpnpm-workspace.yamlalready listspackages/triggers/*, so no workspace glob went stale.⭐ Why no gate caught it: the rule never existed
Not the wrong population, and not a missing clause in an existing rule set. No script in this repo reads
repository.directoryat all.Stated as the measurement rather than a characterisation: across
scripts/, 245 lines contain the tokenrepository, and a grep for any read of a manifestrepository.directoryreturns zero. The only code-shaped uses of the token are CI'sgithub.repositorycontext (process.env.GITHUB_REPOSITORYand therepository:keys building anowner/repoAPI context inrelease-github-releases.mjs). The bulk of the remaining hits are incidental substring matches in file paths and identifiers — the largest single concentration, 27 lines inscripts/engine-double-contract.pinned.json, is the filenamesys-metadata-repository-…test.ts— plus prose in comments. (An earlier revision of this body called them all "CI context or comment prose", which was imprecise; the load-bearing claim, zero reads, is unchanged and was independently confirmed at 0 reads across 6061 tracked code/config files.)The near-miss worth naming is
scripts/check-published-files.mjs. It has exactly the right population — every non-private workspace package — and already parses each manifest and validatesfiles,types,moduleandexportssubpaths. All six of its invariants concern thefileswhitelist: what the tarball contains. The wordrepositoryoccurs 0 times in it. Nothing asks where the tarball says its source lives. The notch is a category, not an off-by-one: this repo gates published package contents and does not gate published package provenance metadata.Two adjacent gates confirm the notch rather than closing it:
check-published-readme-links.mjsresolves links in published markdown (READMEs, not manifests), andcheck-adr-links.mjsmatches only[text](target)markdown links — the stale ADR pointer above is an inline code span, so that gate cannot see it by form.⛔ No gate is changed in this PR. The check is filed as #15991, split out on the instruction in this card's own triage comment ("if the gate turns out to want a decision … land the two string fixes first and split the gate out"). The decision is real and measured: 24 manifests declare nothing, 14 of them publishable (including
@objectstack/trigger-api, a sibling in this very family), so whether declaring the field is mandatory is a policy call, not a correction.Changeset, not
skip-changesetskip-changesetdoes not apply. Its rule is "publishes nothing from any package", and this changes published package metadata:repository.directoryships inside the npm tarball regardless of thefileswhitelist, so a fix that never publishes leaves both package pages 404ing. Apatchchangeset covers both packages. Noskip-changesetlabel is set on this PR.Verification
All runs below are on head
6c2957384.origin/mainmoved during review;dispatch-gates.mjsreported STALE TREE against the older head, soorigin/main(f7db8f4fd) was merged — not rebased, and nothing force-pushed. The merge touched none of this PR's four files, brought in nomerge=os-regenpath, and the re-derivation after it is clean. This PR's diff againstorigin/mainremains exactly those four files.Gate family derived mechanically from the actual changed files —
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, which reads the change set from the merge base itself (4 paths), never hand-built. Every exit code captured immediately after a single redirected command, never through a pipe, and every command given its owncd.All 20 path-derived gates EXIT=0:
adr-anchors,changeset-gate-self-tests,cross-package-test-inputs,doc-authoring,doc-formula-expressions,dts-closure,dual-build-cjs-loads,llms-txt,merge-driver,objectui-changeset,org-identifier,override-consistency,page-declaration-shape,pm-governed-merges,published-files,slot-lookup,test-source-alias,turbo-task-graph,type-source-resolution,workspace-manifest-cycles— plus whole-treenul-bytesEXIT=0.check:doc-formula-expressionsfirst answered 3 = PREREQUISITE NOT MET on the fresh worktree, which is not a pass and is not reported as one; afterturbo run build(73/73 tasks) it re-ran EXIT=0. On the same built tree,check:dts-closureEXIT=0 (164/164 declaration files across 72 built packages) andcheck:dual-build-cjs-loadsEXIT=0 (103 published require entry points across 66 packages load; 619 emitted CommonJS files parse).The check named in dispatch, with its own control:
node scripts/check-adr-0087-registration.mjs --self-testEXIT=0 (325 assertions over real temp git repos), then--base origin/main --head 6c2957384328dbc25e32ed044ab441fba0682ff4EXIT=0 ("adds no declared-breaking changeset, 1 non-breaking changeset seen").Carried forward from the pre-merge head
e0fcc1455, where the diff was identical apart from the changeset prose corrected above:trigger-record-change10 files / 101 tests passed,trigger-schedule4 files / 57 tests passed,typecheckEXIT=0 for both with the script names echoed in the output (a--filtermatching zero scripts exits 0 having run nothing, so the echo is what distinguishes a pass from a no-op). This revision changes only changeset prose, so the suites were not re-run and that is stated rather than implied.scripts/pm/os-verify-lock.sh, which excludes other locked runs and not unlocked sibling work.The census script lives outside the repository tree; no scratch file was on disk during any whole-tree gate run, and the working tree is clean at
6c2957384.🤖 Generated with Claude Code
https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y