feat(admin): the graph read-model, skill enumeration, and record joins (issue #54) - #166
Merged
Conversation
…s (issue #54) The data layer under the trigger/flow graph. Everything the graph will draw becomes readable through read-model.mjs, the admin's one I/O funnel; the view itself is a later slice. Read-model additions, all never-throw, all degrading per folder to a discriminated unreachable, all bounded by the frozen literal-pinned GRAPH_LIMITS: readFolderSkills (object-store enumeration at HEAD via the worker's own selectEntries/keepOnlyDeclaredSkills over one hardened ls-tree, one bounded cat-file per top-level SKILL.md, frontmatter through the gate's own aiTriggerAllows; display-advisory, never a gate decision), readInjectedSkills (working-tree, advisory, the doctor precedent), cronRunStats (raw repeat:<id>:<millis> jobId join with the digits-tail disambiguator), joinRunsToTriggers (persisted triggerIndex with the OQ-008 range guard: a stale index counts unattributed, never lands on whatever entry now occupies the row), observedChainEdges (parentJobId joins folded per flow pair, same-target only per OQ-009, refusals surfaced), and collectGraphInputs (the one dedupe/caps funnel). readTriggers display records now carry the RAW triggers-array index, the identity matched.index counts, so a dropped row leaves a hole rather than renumbering every attribution below it. resolvePaths mirrors the chain caps with defaults imported from the worker. New pure module graph-model.mjs (parseSkillMeta, findSiblingMentions; purity source-regex-tested). findSiblingMentions boundary-matches against the skill-name charset itself rather than \b, because \b calls a hyphen a boundary and fix would fire inside prefix-fix. Worker enablers: ./materialize exports-map subpath, aiTriggerAllows exported, CHAIN_DEPTH_MAX_DEFAULT/CHAIN_MAX_PER_JOB_DEFAULT hoisted as exported consts (loadConfig behaviour unchanged, pinned by test). Specs: DES-ADMIN-VIA-PI-EXTENSION AMENDED, saying out loud what the last three dashboard rows certified as unchanged, because this time it did change: a new read-model surface and new fs/git access. Dashboard fs ban UNCHANGED, checked. .log placement boundary UNCHANGED, checked. DES-AI-TRIGGER-FLOW-GATE UNCHANGED, checked (HEAD-at-display-time is advisory, an unreadable SKILL.md reads as not chainable). Suite in the CI posture: 2100 pass, 0 skipped; admin bundle builds. Signed-off-by: Rob Boerman <robboerman@live.nl>
This was referenced Aug 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Second slice of issue #54: the data layer under the trigger/flow graph. Everything the graph will draw becomes readable through
read-model.mjs, the admin's one I/O funnel. The view itself is a later slice.Read-model additions
All never-throw, all degrading per folder to a discriminated
unreachable, all bounded by the frozen, literal-pinnedGRAPH_LIMITS:readFolderSkills: enumerates a cron folder's committed skills from the git object store at HEAD, via the worker's ownselectEntries/keepOnlyDeclaredSkillsover one hardenedls-tree, plus one boundedcat-fileper top-level SKILL.md; frontmatter read through the gate's own newly exportedaiTriggerAllows. Display-advisory, never a gate decision:DES-AI-TRIGGER-FLOW-GATE's pre-agent-sha truth is untouched, and an unreadable SKILL.md reads as NOT chainable. Sub-skills (.pi/skills/group/sub/SKILL.md) are carried asisSubwith their group; they can never be flows.readInjectedSkills: arun.skillsDirworking-tree listing, advisory on the doctor precedent (host files have no object store). Surfaces the OQ-022 badge fact: an injected skill carryingai-trigger: allowis a silent no-op.cronRunStats: run counts + last outcome per scheduler id, joined on the RAWrepeat:<id>:<millis>jobId with the digits-tail disambiguator (themakeFindPreviousRundoctrine) and regex-escaped ids.joinRunsToTriggers: forge records joined via the persistedtriggerIndex(PR feat(worker): persist trigger attribution on the run record (issue #54) #165), with the OQ-008 range guard: a stale index counts underunattributed, never lands on whatever entry now occupies the row. Index 0 attributes.observedChainEdges:parentJobIdjoins folded per (parentFlow, childFlow, target), same-target only (OQ-009: cross-folder is unrepresentable, so it is never drawn), self-chains included,chainRefusedsurfaced per parent flow, capped with atruncatedflag.collectGraphInputs: the one folder/skillsDir dedupe + caps funnel, so the dashboard seam and the CLI subcommand cannot diverge on it.Also:
readTriggersdisplay records now carry the raw triggers-array index (the identitymatched.indexcounts, cron entries and unusable rows included, so a dropped row leaves a hole rather than renumbering every attribution below it), andresolvePathsmirrors the chain caps with defaults imported from the worker, so the graph can never state a cap the worker does not enforce.New pure module
admin/src/graph-model.mjs(purity source-regex-tested):parseSkillMeta(frontmatter name/description, the gate scanner's block-isolation discipline, values clipped never refused) andfindSiblingMentions(the heuristic half of a potential chain edge). Mentions are boundary-matched against the skill-name charset itself rather than\b, because\bcalls a hyphen a boundary andfixwould fire insideprefix-fix.Worker enablers
./materializeexports-map subpath (probed inload.test.mjs's fixture),aiTriggerAllowsexported, andCHAIN_DEPTH_MAX_DEFAULT/CHAIN_MAX_PER_JOB_DEFAULThoisted as exported consts.loadConfigbehaviour unchanged, pinned by test.Specs (same PR)
DES-ADMIN-VIA-PI-EXTENSIONAMENDED, and the revision row says out loud what the last three dashboard rows certified as unchanged, because this time it did change: a new read-model surface and new fs/git access. The dashboard's source-regex fs ban UNCHANGED, checked (every new spawn and read lives in read-model.mjs). The.logplacement boundary UNCHANGED, checked (the folds read.jsonrecords only).DES-AI-TRIGGER-FLOW-GATEUNCHANGED, checked.Tests
Suite in the CI posture: 2100 pass, 0 skipped;
node admin/build.mjsbuilds. Each new function ships the 6-case degradation set (thereadStagedPackagesmodel),GRAPH_LIMITSis literal-pinned frozen, the exec fake throws on anything unrouted (the poller-fake doctrine), and the negative claims are explicit: a stale index never misattributes, cross-target pairs are dropped, deny is not dangling, an unread skill is not chainable.