Skip to content

feat(admin): the graph read-model, skill enumeration, and record joins (issue #54) - #166

Merged
edgehero merged 1 commit into
mainfrom
feat/graph-read-model
Aug 11, 2026
Merged

feat(admin): the graph read-model, skill enumeration, and record joins (issue #54)#166
edgehero merged 1 commit into
mainfrom
feat/graph-read-model

Conversation

@edgehero

Copy link
Copy Markdown
Owner

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-pinned GRAPH_LIMITS:

  • readFolderSkills: enumerates a cron folder's committed skills from the git object store at HEAD, via the worker's own selectEntries/keepOnlyDeclaredSkills over one hardened ls-tree, plus one bounded cat-file per top-level SKILL.md; frontmatter read through the gate's own newly exported aiTriggerAllows. 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 as isSub with their group; they can never be flows.
  • readInjectedSkills: a run.skillsDir working-tree listing, advisory on the doctor precedent (host files have no object store). Surfaces the OQ-022 badge fact: an injected skill carrying ai-trigger: allow is a silent no-op.
  • cronRunStats: run counts + last outcome per scheduler id, joined on the RAW repeat:<id>:<millis> jobId with the digits-tail disambiguator (the makeFindPreviousRun doctrine) and regex-escaped ids.
  • joinRunsToTriggers: forge records joined via the persisted triggerIndex (PR feat(worker): persist trigger attribution on the run record (issue #54) #165), with the OQ-008 range guard: a stale index counts under unattributed, never lands on whatever entry now occupies the row. Index 0 attributes.
  • observedChainEdges: parentJobId joins folded per (parentFlow, childFlow, target), same-target only (OQ-009: cross-folder is unrepresentable, so it is never drawn), self-chains included, chainRefused surfaced per parent flow, capped with a truncated flag.
  • collectGraphInputs: the one folder/skillsDir dedupe + caps funnel, so the dashboard seam and the CLI subcommand cannot diverge on it.

Also: readTriggers display records now carry the raw triggers-array index (the identity matched.index counts, cron entries and unusable rows included, so a dropped row leaves a hole rather than renumbering every attribution below it), and resolvePaths mirrors 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) and findSiblingMentions (the heuristic half of a potential chain edge). Mentions are boundary-matched 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 (probed in load.test.mjs's fixture), aiTriggerAllows exported, and CHAIN_DEPTH_MAX_DEFAULT/CHAIN_MAX_PER_JOB_DEFAULT hoisted as exported consts. loadConfig behaviour unchanged, pinned by test.

Specs (same PR)

DES-ADMIN-VIA-PI-EXTENSION AMENDED, 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 .log placement boundary UNCHANGED, checked (the folds read .json records only). DES-AI-TRIGGER-FLOW-GATE UNCHANGED, checked.

Tests

Suite in the CI posture: 2100 pass, 0 skipped; node admin/build.mjs builds. Each new function ships the 6-case degradation set (the readStagedPackages model), GRAPH_LIMITS is 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.

…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>
@edgehero
edgehero merged commit cb3ec3c into main Aug 11, 2026
17 of 18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant