{"name":"groundwork","release":"b155517c3eaff44615027b9f0d74299d7138c775","docs":"https://ntent.app/llms.txt","tiers":[{"tier":1,"shape":"Prototypes & landing pages","asked":"Nothing","plan":"https://ntent.app/r/tier/1.json","items":["constitution","symlink-claude","pre-commit","hooks-path","check-env","decisions"]},{"tier":2,"shape":"A product with a fixed scope","asked":"\"Are we on track?\"","plan":"https://ntent.app/r/tier/2.json","items":["eslint-rules","rules-probe","check-probes","waivers","check-tokens","check-api-routes","verify-fixtures","error-to-surface","error-to-surface-test","contract","access","check-contract-coverage","check-contract-coverage-test","doctor","session-status"]},{"tier":3,"shape":"Apps with a shared design system","asked":"\"Who agreed to that?\"","plan":"https://ntent.app/r/tier/3.json","items":["check-locale-keys","check-stray-components","import-boundaries","path-scoped-rules","source-policy","rule-provenance","entitlements"]},{"tier":4,"shape":"Products built for the long term","asked":"\"Why is it like this?\"","plan":"https://ntent.app/r/tier/4.json","items":["build-log-queue","build-log-ignore","post-commit","session-state","research-protocol","generated-header","graduating-warning"]}],"capabilities":[{"id":"ai","question":"A model’s output reaches a user","items":["eval-types","eval-judge","eval-report","eval-harness"],"example":"https://ntent.app/r/tier/1.json?with=ai"},{"id":"signin","question":"People sign in, and what they see depends on who they are","items":["contract","access"],"example":"https://ntent.app/r/tier/1.json?with=signin"},{"id":"paid","question":"Some of it is paid for","items":["contract","entitlements"],"example":"https://ntent.app/r/tier/1.json?with=paid"},{"id":"locales","question":"It ships in more than one language","items":["check-locale-keys"],"example":"https://ntent.app/r/tier/1.json?with=locales"}],"tasks":[{"id":"agree-one-model","name":"Agree on the product model","arriving":"Bring requirements and call notes into one agreed source.","plan":"https://ntent.app/r/task/agree-one-model.json"},{"id":"screens-from-the-model","name":"Create the screen list","arriving":"Turn the agreed model into a list of screens and journeys.","plan":"https://ntent.app/r/task/screens-from-the-model.json"},{"id":"build-a-screen","name":"Build a screen","arriving":"Build a screen from the agreed fields, stages, and rules.","plan":"https://ntent.app/r/task/build-a-screen.json"},{"id":"model-or-code","name":"Handle a change during the build","arriving":"Decide where a change belongs: the model, the design system, or the code.","plan":"https://ntent.app/r/task/model-or-code.json"},{"id":"add-a-check","name":"Add an automated check","arriving":"Turn an agreed rule into a check the whole team can run.","plan":"https://ntent.app/r/task/add-a-check.json"},{"id":"check-progress","name":"Report project progress","arriving":"Prepare a clear progress update with coverage and named gaps.","plan":"https://ntent.app/r/task/check-progress.json"}],"artefacts":[{"id":"constitution","name":"Shared project instructions","file":"AGENTS.md","kind":"file","tier":1,"lanes":["design","engineering","product"],"enforcement":"review","catches":"Everyone inventing their own conventions, and every assistant inventing a different set again.","target":"AGENTS.md","targetNote":"point CLAUDE.md at it with a one-line @AGENTS.md import","merge":false,"customise":"It is a template. Replace every <angle bracket>, delete the sections marked for tiers above yours, and cut any row of the enforcement table whose command this repo does not have.","verify":"Ask an assistant \"what are the rules in this repo\" and it answers from the file.","content":"https://ntent.app/r/f/constitution","sha256":"4ad91e85a752a81387840681f5da12246530202cb4d9e9170eb40621324e8ebc"},{"id":"symlink-claude","name":"Shared instructions for coding agents","kind":"command","tier":1,"lanes":["engineering"],"enforcement":"none","catches":"2 rule files that disagree, because one tool reads CLAUDE.md and another reads AGENTS.md.","merge":false,"needs":["AGENTS.md"],"verify":"Run it twice: the second run says there is nothing to do. Then start a session and ask the assistant to name a rule that only exists in AGENTS.md. In Claude Code, /context lists CLAUDE.md under Memory files. Copy a line of AGENTS.md into CLAUDE.md and run it again: it names that line and leaves it where it is.","content":"https://ntent.app/r/f/symlink-claude","sha256":"390916ef5802ea148badbf0e934bd6a3ba6c140408f2e2a3cbd6ea1e3146859a"},{"id":"pre-commit","name":"Pre-commit checks","file":"pre-commit","kind":"file","tier":1,"lanes":["engineering"],"enforcement":"gate","catches":"A mistake caught an hour later on the build server, after someone has already spent time reviewing it. This catches it on your machine, before the commit.","target":"scripts/git-hooks/pre-commit","targetNote":"make it executable","merge":false,"verify":"Stage a file that fails a check this tier installs and the commit is refused, naming the check and the fix. At tier 1 that is check-env: add a variable to the env schema and not to .env.example. The hardcoded-colour case needs the tier 2 lint rules.","content":"https://ntent.app/r/f/pre-commit","sha256":"93626e41aa0df823c263ceec1b231b50fe409d79a36a28d1b5b4c54f233d1f6a"},{"id":"hooks-path","name":"Git hook setup","kind":"snippet","tier":1,"lanes":["engineering"],"enforcement":"none","catches":"Store hook setup in git so every contributor and coding agent runs the same checks.","target":"package.json","targetNote":"inside the \"scripts\" block","merge":true,"verify":"Run pnpm install, then `git config core.hooksPath` prints scripts/git-hooks. A fresh clone gets the same answer with no manual step. Leave .git/hooks alone: it may hold hooks somebody else installed.","content":"https://ntent.app/r/f/hooks-path","sha256":"7155c50ee73ae905e6fd1104dca859559de83208a86cb4fd968472d31268a728"},{"id":"check-env","name":"Environment variable checks","file":"check-env.mjs","kind":"file","tier":1,"lanes":["engineering"],"enforcement":"gate","catches":"A setting the app needs that is missing from .env.example, so someone who downloads the repo cannot start it and has no idea why. Also the reverse, and code that reads process.env directly instead of through the schema.","target":"scripts/check-env.mjs","merge":false,"customise":"Point CONFIG.schemaFile at this repo’s env schema, CONFIG.exampleFile at its example file, and CONFIG.sourceDirs at the directories it keeps source in, if they are not src/env.ts, .env.example and src. The pre-commit hook reads those three back out of the file, so moving them keeps the gate wired.","verify":"Add a variable to the schema, do not add it to .env.example, and the check names it.","content":"https://ntent.app/r/f/check-env","sha256":"1930b023e3cf1f54d9c0cabc5720bc9aa5509b10d6d1d9ae05b9212064ba56ad"},{"id":"decisions","name":"Decision log","kind":"snippet","tier":1,"lanes":["design","engineering","product"],"enforcement":"review","catches":"The same decision being re-made in month 6 because nobody remembers the reason for the first one.","target":"DECISIONS.md","merge":false,"customise":"The first entry is an example from a billing app: delete it. Then write the decisions this project has actually made, one entry each, and label any whose source is memory rather than a thread or a call as \"(from memory)\". A fresh project may have none yet, and a file with only the template entry in it is correct.","verify":"Every entry names a real decision, who agreed it and when. Nothing in the file is invented, and an entry reconstructed from memory says so.","content":"https://ntent.app/r/f/decisions","sha256":"64d083971ed620984d7f0d0bb99d59b52982caf8d703764dc23f9218c8d80e5b"},{"id":"eslint-rules","name":"Design convention lint rules","file":"eslint-rules.mjs","kind":"file","tier":2,"lanes":["design","engineering"],"enforcement":"gate","catches":"Catch hardcoded colours, font sizes, and interface strings. Flag spacing that does not adapt to right-to-left layouts.","target":"scripts/eslint-rules.mjs","targetNote":"import it from eslint.config.mjs","merge":false,"needs":["ESLint flat config"],"verify":"Write text-[13px] in a component. pnpm lint fails and points at it.","content":"https://ntent.app/r/f/eslint-rules","sha256":"c5955230d11e8c7aec73feb82ada6afa0c6fbbc9fa2af9297392bfbb8c87a4ee"},{"id":"check-probes","name":"Lint rule probes","file":"check-probes.mjs","kind":"file","tier":2,"lanes":["engineering"],"enforcement":"gate","catches":"Catch lint rules that have quietly stopped working: they miss what they should catch, complain about good code, or are switched off in one package.","target":"scripts/check-probes.mjs","merge":false,"customise":"CONFIG.PACKAGES ships with one entry for a single-package repo. In a monorepo add one entry per package that lints, each naming a real source file in it.","needs":["eslint-rules.mjs","rules.probe.tsx"],"verify":"Break a rule pattern on purpose and the probe fails even though lint passes. Take the rules out of one package config and it names that package and says how many rules run nowhere in it.","content":"https://ntent.app/r/f/check-probes","sha256":"398d392979f7f550187391c5842fd2ed0ff7094a2667ef1545061644fafd4c13"},{"id":"rules-probe","name":"Probe fixtures","file":"rules.probe.tsx","kind":"file","tier":2,"lanes":["engineering"],"enforcement":"none","catches":"A rule everyone believes is on, which has been silently off since a config change.","target":"scripts/probes/rules.probe.tsx","merge":false,"verify":"check-probes reports one hit per rule, and zero false hits.","content":"https://ntent.app/r/f/rules-probe","sha256":"8a5f7d6bd06ca38e1b0011d29671a0565065fa0348eed18aa1b8b9f01e7647ff"},{"id":"waivers","name":"Reviewed rule exceptions","file":"waivers.mjs","kind":"file","tier":2,"lanes":["design","engineering"],"enforcement":"none","catches":"Keep rule exceptions visible to reviewers. Record the reason, owner, and expiry so exceptions can be checked later.","target":"scripts/lib/waivers.mjs","merge":false,"needs":["a checker of your own to import it"],"verify":"Waive a line with no reason and the run prints NO REASON GIVEN against it. Fix the underlying value and leave the waiver, and the next run names it as stale and tells you to delete it. Date one `until` yesterday and the finding comes back, naming the waiver that ran out.","content":"https://ntent.app/r/f/waivers","sha256":"33cfef32b39fb292804badd37d4451118be1c0b5645dd33f1b99da92803b19f3"},{"id":"check-tokens","name":"Design token checks","file":"check-tokens.mjs","kind":"file","tier":2,"lanes":["design","engineering"],"enforcement":"gate","catches":"Catch design token names that do not exist in the stylesheet, so the style silently does nothing. Also catch colours and sizes typed in by hand instead of taken from the tokens.","target":"scripts/check-tokens.mjs","merge":false,"customise":"Point CONFIG.stylesheets at the generated stylesheet this repo actually ships, and list a family in CONFIG.ownedFamilies only once your tokens replace the framework’s scale for it.","needs":["a generated stylesheet","waivers.mjs"],"verify":"Write text-fg-nope next to a real text-fg-muted. The check names it, says it resolves to nothing, and lists the nearest real tokens. Delete every token in a family and it says that family is no longer checked rather than passing.","content":"https://ntent.app/r/f/check-tokens","sha256":"02acb84dfd1f4a3cc6dd6ca53262990a70a5d501fede77411d199d932eb43ee3"},{"id":"check-api-routes","name":"API route checks","file":"check-api-routes.mjs","kind":"file","tier":2,"lanes":["engineering"],"enforcement":"gate","catches":"A server endpoint that accepts form data without checking it, does not check who is asking, or has no limit on how often it can be called. Your forms talk to these directly, so a gap here is a gap in the UI.","target":"scripts/check-api-routes.mjs","merge":false,"customise":"Set CONFIG.routeDirs to where this repo keeps its handlers, and add your own validation, auth and rate-limit helper names to the three matcher lists.","verify":"Add a POST handler with no schema parse. The check names the file and the missing piece.","content":"https://ntent.app/r/f/check-api-routes","sha256":"f6425cede5aa0af7c8be8feebfaf909183ce86c07a7a4210664f08720cac2201"},{"id":"verify-fixtures","name":"Real data validation","file":"verify-fixtures.mjs","kind":"file","tier":2,"lanes":["design","engineering"],"enforcement":"gate","catches":"The app expects data in one shape and the server now sends another. On screen that looks like a column of NaN, an empty list when there is data, or Invalid Date.","target":"scripts/verify-fixtures.mjs","targetNote":"put real captured payloads in fixtures/","merge":false,"customise":"It ships pointed at fixtures/ and src/lib/schemas/index.js. Change CONFIG.fixtureDir and CONFIG.schemaModule if this repo keeps either somewhere else.","needs":["Zod schemas","at least one captured payload"],"verify":"fixtures/invoice.json parses with InvoiceSchema. Change a field type in the schema and the check prints the field path and the reason. Replace a fixture with `[]` and it fails: an empty batch exercises nothing.","content":"https://ntent.app/r/f/verify-fixtures","sha256":"7cf6ccf2e953af6201f4b9a5232b34238ccc194f2f38ea6f92fe7f6dacfa69ce"},{"id":"error-to-surface","name":"Message placement rules","file":"errorToSurface.ts","kind":"file","tier":2,"lanes":["design","engineering"],"enforcement":"review","catches":"The same message written 2 different ways in one product. Or a pop-up toast for a problem that is still there after the toast fades away.","target":"src/lib/errorToSurface.ts","merge":false,"verify":"Every message in the product resolves to one of 4 surfaces: full-page empty state, inline, persistent banner, toast.","content":"https://ntent.app/r/f/error-to-surface","sha256":"ce977002088a91103655ff2d19149fe3eee87eafba1612b3725bec8e2bca7234"},{"id":"error-to-surface-test","name":"Message rule tests","file":"errorToSurface.test.mjs","kind":"file","tier":2,"lanes":["engineering"],"enforcement":"none","catches":"A message rule that reads well in a doc and does something else in code.","target":"src/lib/errorToSurface.test.mjs","merge":false,"verify":"node --test passes.","content":"https://ntent.app/r/f/error-to-surface-test","sha256":"ea86119bd4a1a71c9c8e74a16a7f00070e4bf8bd80b1163df3e4c61591ba6f76"},{"id":"access","name":"Access rules","kind":"snippet","tier":2,"lanes":["product","design","engineering"],"enforcement":"review","catches":"Define permissions and row access in one place. Use them to design gated navigation, empty lists, and disabled actions.","target":"contract.json","targetNote":"at the top level, after actors","merge":true,"verify":"Point at a new route and ask which matrix row permits it. If the answer needs a conversation, the row is missing.","content":"https://ntent.app/r/f/access","sha256":"bd42be7d5d74ab5e2208b294fa3f5c1e3f69f8e757c6e0f53bf994c4ff25e796"},{"id":"contract","name":"Product intent model","file":"contract.json","kind":"file","tier":2,"lanes":["product","design","engineering"],"enforcement":"none","catches":"\"Mostly done\" as an answer. With this you can say the screens lift 80% of the model, here are the 3 named gaps, and one journey has no UI at all.","target":"contract.json","merge":false,"customise":"It is an example billing product. Keep the shape and replace the actors, entities, journeys, rules, constraints and open questions with this product’s own. Delete the example acceptance records: they describe journeys you have not built.","verify":"The coverage check runs and prints a number, and `--refs` says the references resolve. Start with entities and journeys; add a pillar when you can name the screen it changes. Mark a journey built only with its acceptance record beside it.","content":"https://ntent.app/r/f/contract","sha256":"bc7d9681bfb24c99955cd20c06b4d8088fe232e55aee4d2274b7fd8b99ac0a47"},{"id":"check-contract-coverage","name":"Contract coverage","file":"check-contract-coverage.mjs","kind":"file","tier":2,"lanes":["product","design","engineering"],"enforcement":"nag","catches":"Shows how much of the model is in the code, as 3 separate numbers: fields in the types, fields in real data, and fields the screens mention. Also catches the model contradicting itself: duplicate ids, links to things that do not exist, and a journey marked built while its questions are open or with no note of who saw it work.","target":"scripts/check-contract-coverage.mjs","merge":false,"customise":"The maps at the top of the file describe an example billing app. Replace CONFIG.entityTypeMap, CONFIG.entityUiMap and CONFIG.fieldAliases with this product’s own entities, and point typeDirs/fixtureDirs/uiDirs at its directories.","needs":["contract.json","TypeScript"],"verify":"It prints a table with a percentage per entity, names the gaps, and says how many of the contract’s fields the number is even about. Under --check an unmapped entity fails until you map it or excuse it with a reason. Set minCoverage just below your current number so it fails when coverage goes backwards. `--refs` is the gate half: it runs in milliseconds, exits non-zero on a duplicate id or a broken reference, and every finding names its fix. None of it says a journey works: that is the journey’s state, set by hand, and `--refs` refuses `built` unless an acceptance record sits under it: who exercised it, when, at which commit, against what evidence.","content":"https://ntent.app/r/f/check-contract-coverage","sha256":"a851e9b9aec9684f33772fe529715f5cb02077258b8f3e824eaf2fe1070cad0f"},{"id":"check-contract-coverage-test","name":"Contract coverage tests","file":"check-contract-coverage.test.mjs","kind":"file","tier":2,"lanes":["engineering"],"enforcement":"none","catches":"Test that each contract check catches its target issue and stays quiet on valid cases.","target":"scripts/check-contract-coverage.test.mjs","merge":false,"needs":["check-contract-coverage.mjs","contract.json"],"verify":"node --test passes. Break one reference in the contract by hand and exactly one test goes red, naming the check that caught it.","content":"https://ntent.app/r/f/check-contract-coverage-test","sha256":"9aa9fc70e644cfece52612e2dfb2c2ea4e5939abb29efb0714c5505ea9b2abe4"},{"id":"doctor","name":"Project health check","file":"doctor.sh","kind":"file","tier":2,"lanes":["design","engineering"],"enforcement":"none","catches":"Find broken local setup and missing generated files. Give new contributors a clear list of issues to fix.","target":"scripts/doctor.sh","targetNote":"make it executable","merge":false,"verify":"Run it on a fresh clone. Every line is a pass, or names the one thing to fix.","content":"https://ntent.app/r/f/doctor","sha256":"6b60694f4a9001e582a74ee1b100291b2ed392b5ce15dcd3821cec0135ed624a"},{"id":"session-status","name":"Session status report","file":"session-status.sh","kind":"file","tier":2,"lanes":["engineering"],"enforcement":"nag","catches":"Report issues that cannot block offline work. Flag generated files that are older than their sources.","target":"scripts/session-status.sh","targetNote":"make it executable","merge":false,"verify":"It prints when someone opens the repo, which is the moment they can act on it. Touch a token source without rebuilding and it tells you that you are looking at the previous build.","content":"https://ntent.app/r/f/session-status","sha256":"7c0f359a09eb9a9b85316db13374836f66ae9ab4bf95e3925abe8cb253157472"},{"id":"path-scoped-rules","name":"Rules by file path","file":"copy-a-sibling.md","kind":"file","tier":3,"lanes":["design","engineering"],"enforcement":"review","catches":"Load local rules only where they apply. Keep shared project instructions short enough to read.","target":".claude/rules/copy-a-sibling.md","targetNote":"one file per concern, named after the rule","merge":false,"customise":"It is a template. Set the paths it applies to and write the rule in this repo’s own terms.","needs":["AGENTS.md"],"verify":"Open a file the rule’s paths cover and ask the assistant to state the rule. It answers without being shown the file.","content":"https://ntent.app/r/f/path-scoped-rules","sha256":"f65553cbae037faf5348865e99d2777d625a9a092f96063881b8e62df7729cee"},{"id":"check-locale-keys","name":"Translation key checks","file":"check-locale-keys.mjs","kind":"file","tier":3,"lanes":["design","engineering"],"enforcement":"gate","catches":"A t('key') with no entry in the base locale. A key in the base locale missing from a translation. An incomplete plural set.","target":"scripts/check-locale-keys.mjs","merge":false,"needs":["more than one language, or the certainty you will have one"],"verify":"Add a t() call with a new key and no entry. The commit is refused. Before messages/ exists it says there is nothing to check yet and lets the commit through, so taking the locales capability on day one costs nothing.","content":"https://ntent.app/r/f/check-locale-keys","sha256":"6a747cbbd67c0ff86e9b20e8ac90f583bd3b6e65144ad07bce3fce2722331f97"},{"id":"check-stray-components","name":"Shared component checks","file":"check-stray-components.mjs","kind":"file","tier":3,"lanes":["design","engineering"],"enforcement":"gate","catches":"Find shared components left in route folders and duplicate component names. Require a reason and an expiry date for exceptions.","target":"scripts/check-stray-components.mjs","merge":false,"customise":"Point CONFIG at this repo’s route and shared-component directories if they are not src/app and src/components.","verify":"Import a private component from a second route and the check tells you to promote it. Allowlist it and the reason and date print on every run afterwards, rather than the finding disappearing. Backdate the entry and it fails.","content":"https://ntent.app/r/f/check-stray-components","sha256":"bbfb160e2debc168aa8da205eee38171db4f8c6340752e0d194db87cee880774"},{"id":"entitlements","name":"Product entitlements","kind":"snippet","tier":3,"lanes":["product","design","engineering"],"enforcement":"review","catches":"Define paid features and quotas. Cover access near the limit, at the limit, and after a downgrade.","target":"contract.json","targetNote":"at the top level. A plan is not an entity","merge":true,"verify":"Every gate names what a person who has not paid sees. Ask the model which journeys have a paywall in them and get an answer before QA does.","content":"https://ntent.app/r/f/entitlements","sha256":"d806ab56682e275b467b1a0d541fe80090c1eb21041787688b5c723142b3d624"},{"id":"source-policy","name":"Source priority","kind":"snippet","tier":3,"lanes":["product","design","engineering"],"enforcement":"review","catches":"Set a clear priority when sources disagree. Record the resolution so the team can find it later.","target":"contract.json","targetNote":"at the top level","merge":true,"verify":"2 sources disagree, and the argument takes 30 seconds instead of a meeting.","content":"https://ntent.app/r/f/source-policy","sha256":"56c7f93efbd1cba9dbc3321ddcac318e3892192f829f6260f5b424cbf29cd8c1"},{"id":"rule-provenance","name":"Rule approvals","kind":"snippet","tier":3,"lanes":["product","design","engineering"],"enforcement":"review","catches":"Keep the approval source beside each rule. Make it clear who agreed to it and when.","target":"contract.json","targetNote":"on each entry in rules","merge":true,"verify":"Every rule names a person and a date, and quotes them where the wording matters.","content":"https://ntent.app/r/f/rule-provenance","sha256":"171d168911eb0292a0c613680f2708268f1c52c63bbbad48fdc6038565474a2a"},{"id":"post-commit","name":"Automatic build log","file":"post-commit","kind":"file","tier":4,"lanes":["engineering"],"enforcement":"none","catches":"Nobody being able to answer \"what changed last week\" without reading 200 commit messages.","target":"scripts/git-hooks/post-commit","targetNote":"make it executable","merge":false,"needs":["docs/build-log/_pending.md ignored by git"],"verify":"Commit twice. docs/build-log/_pending.md has 2 stubs and `git status` is clean, because the queue is ignored.","content":"https://ntent.app/r/f/post-commit","sha256":"368cdf8bf9de967afbba8d57230b58376f1f9c962ceb5badd44219c9d131f5e4"},{"id":"build-log-queue","name":"Build log queue","kind":"snippet","tier":4,"lanes":["engineering"],"enforcement":"none","catches":"The build log silently recording nothing, because the file it writes to was never created.","target":"docs/build-log/_pending.md","merge":false,"verify":"The file exists before the first commit, so the very first stub lands rather than being dropped.","content":"https://ntent.app/r/f/build-log-queue","sha256":"b14245a1e599ce5cf217120b99a112756463b3825cb914a2ac2e09b39552a500"},{"id":"build-log-ignore","name":"Keep the queue out of git","kind":"snippet","tier":4,"lanes":["engineering"],"enforcement":"none","catches":"The build log writing into a file git tracks, so every commit leaves a new change behind and the repo never looks clean.","target":".gitignore","targetNote":"one line, anywhere in the file","merge":true,"needs":["docs/build-log/_pending.md"],"verify":"Commit twice, then run git status. The stubs are there and the tree is clean.","content":"https://ntent.app/r/f/build-log-ignore","sha256":"92cd63a1c81ee4867e3bac674089fc50ddc4c45f46b108c2b0d5e1bf3b340b30"},{"id":"generated-header","name":"Generated file markers","kind":"snippet","tier":4,"lanes":["engineering"],"enforcement":"review","catches":"Mark files that can be regenerated safely. Keep custom logic out of files that the generator replaces.","targetNote":"the first 3 lines of every generated file","merge":true,"verify":"Run the generator twice. Nothing hand-written is lost.","content":"https://ntent.app/r/f/generated-header","sha256":"ef6fbe20c53503f667afbe71abd2f80fcbc40c361b14a26d56483ef0db5b6973"},{"id":"eval-types","name":"Evaluation thresholds","file":"types.mjs","kind":"file","capability":"ai","lanes":["engineering"],"enforcement":"none","catches":"The pass mark for AI answers copied into 3 files, so raising it means hunting for every copy instead of changing one line.","target":"evals/types.mjs","merge":false,"verify":"Every threshold the suite uses is in this one file, and changing your bar is a reviewable diff. It also says which failures the pass rate is never allowed to absorb: critical cases, deterministic checks, and results the harness could not measure.","content":"https://ntent.app/r/f/eval-types","sha256":"058db71c64e1cb4b673a4e0757a2706c81c389cde3b225bf0f92e268140f2e9d"},{"id":"eval-report","name":"Evaluation scoring and report","file":"report.mjs","kind":"file","capability":"ai","lanes":["engineering"],"enforcement":"none","catches":"A single pass rate that says something broke but not what. A grading result with no scores in it counting as a pass. One answer in 20 saying something it must never say, while the report still shows 95% and PASS.","target":"evals/report.mjs","merge":false,"needs":["evals/types.mjs"],"verify":"The report breaks down by segment and by category. Feed it a result whose gating dimensions are missing and the question fails rather than passes. Feed it nineteen passes and one case marked critical that failed, and the suite fails at 95%.","content":"https://ntent.app/r/f/eval-report","sha256":"785c7632304b27e2bb7a2ca625926daedd0ae4ca14455132c62b776445ebc53c"},{"id":"eval-harness","name":"Model output evaluation tests","file":"eval.test.mjs","kind":"file","capability":"ai","lanes":["engineering"],"enforcement":"none","catches":"An AI feature that got worse and nobody noticed, because the only test was somebody trying it once.","target":"evals/eval.test.mjs","merge":false,"needs":["an AI feature that reaches users","evals/judge.mjs","evals/report.mjs","evals/types.mjs"],"verify":"node --test evals/eval.test.mjs passes. That is the harness proving ITSELF, on stubbed judge responses: it is what makes the scoring trustworthy, not a measurement of your feature. Running your own questions is the integration step, and it is yours to write: give judge() a judgeFn that calls your provider, feed it real responses with their retrieved sources, and put the summary somewhere you keep.","content":"https://ntent.app/r/f/eval-harness","sha256":"e5905f184beb70e1615db91db28ab5e40462bb447590f41e29a7d6d0b6d2eced"},{"id":"eval-judge","name":"Evaluation judge prompts","file":"judge.mjs","kind":"file","capability":"ai","lanes":["engineering"],"enforcement":"none","catches":"One vague grading prompt that scores 5 different things at once, and tells you nothing useful about any of them.","target":"evals/judge.mjs","merge":false,"verify":"Each category reports its own score, and the summary breaks down by segment.","content":"https://ntent.app/r/f/eval-judge","sha256":"0655f9776d2ef93cce38307ae0fef89fc4dbcb967844cc169764b2e44ae5f977"},{"id":"import-boundaries","name":"Import boundaries","kind":"snippet","tier":3,"lanes":["engineering"],"enforcement":"gate","catches":"Server code pulled into code that runs in the browser, which can send your secret keys to every visitor. Also real code depending on something in the experiments folder.","target":"eslint.config.mjs","merge":true,"verify":"Import a server util into a \"use client\" file. Lint refuses it by name.","content":"https://ntent.app/r/f/import-boundaries","sha256":"09badbced1f7ecd4eeafc2889e2abca20e9281e92f119b60882afd6b0f6f18a9"},{"id":"graduating-warning","name":"Gradual rule rollout","kind":"snippet","tier":4,"lanes":["engineering"],"enforcement":"nag","catches":"A new rule that cannot be turned on because 200 existing files break it, so it never gets turned on at all.","target":"eslint.config.mjs","targetNote":"at the top of the file, beside the rules block","merge":true,"verify":"Lint warns on the listed files and errors everywhere else. Move your clock past the date without emptying the list and the config refuses to load, naming what is left.","content":"https://ntent.app/r/f/graduating-warning","sha256":"1d6eb5571a8e100ea4ffef2edccdd4f4a07db34bda9ee2127bd35a26ea350513"},{"id":"session-state","name":"Session handoff","kind":"snippet","tier":4,"lanes":["engineering","design"],"enforcement":"review","catches":"Every session starting with 20 minutes of rediscovering what was in flight. Keep a hard size cap, or it becomes a second unmaintained doc.","target":"STATE.md","merge":false,"verify":"Open it cold after a week and know the next action in under a minute.","content":"https://ntent.app/r/f/session-state","sha256":"d665df43c4ba2c33065251161cf88d9683d18004527b2ebbb563e5de252bf547"},{"id":"research-protocol","name":"Research instructions","kind":"snippet","tier":4,"lanes":["product","design","engineering"],"enforcement":"review","catches":"Someone building from the first document they found, which is usually the most detailed one rather than the agreed one.","target":"AGENTS.md","targetNote":"as a new section","merge":true,"verify":"A new joiner reads 4 things and starts, instead of reading everything and stalling.","content":"https://ntent.app/r/f/research-protocol","sha256":"9d6b44c3f88602f4696e9f61a19a17b663319a35f02c476eb04d7d41cad5e120"},{"id":"prompt-actor-responsibilities","name":"Find out what one role actually does","kind":"prompt","tier":2,"lanes":["design","product"],"enforcement":"none","catches":"Designing 6 screens for a role before noticing it only has 3 jobs.","merge":false,"needs":["a reviewed intent model"],"verify":"You get a flat list you can turn straight into a screen inventory.","content":"https://ntent.app/r/f/prompt-actor-responsibilities","sha256":"513b25286c17bea6d06958281670f89d67ad5cfd2fd6cffb2f97f93082f84f36"},{"id":"prompt-entity-stages","name":"Get the stages and what blocks each one","kind":"prompt","tier":2,"lanes":["design","engineering"],"enforcement":"none","catches":"A screen that renders 3 of the 5 stages an entity can be in, so 2 states have no visual treatment and nobody notices until the demo.","merge":false,"needs":["a reviewed intent model"],"verify":"A table you can read a badge colour and a button state off, row by row.","content":"https://ntent.app/r/f/prompt-entity-stages","sha256":"f3314ff6b75a8bd6ec84756783c1009cc44ecd88d1d7a965b5660975b71a85a2"},{"id":"prompt-screen-inventory","name":"Turn responsibilities into a screen list","kind":"prompt","tier":2,"lanes":["design"],"enforcement":"none","catches":"A backlog nobody agrees on, because the screens were never derived from anything.","merge":false,"needs":["a reviewed intent model"],"verify":"A table of route, screen name and fields that becomes your progress tracker.","content":"https://ntent.app/r/f/prompt-screen-inventory","sha256":"19a2e78dca3802a4694c34f88e649261203852b3b5f1e78b1af40bcb03d27a81"},{"id":"prompt-map-journey","name":"Turn a journey into a screen sequence","kind":"prompt","tier":2,"lanes":["design"],"enforcement":"none","catches":"A flow that works until it fails, because nobody asked what happens when validation fails or a condition blocks the next step.","merge":false,"verify":"Every step names a route, a pattern, and what happens when it goes wrong.","content":"https://ntent.app/r/f/prompt-map-journey","sha256":"b187f05a423c5a3c4bcec7afe72561e233b9219f968c80ddf49cf33300780182"},{"id":"prompt-build-screen","name":"Build a screen","kind":"prompt","tier":2,"lanes":["design","engineering"],"enforcement":"none","catches":"A screen that looks right and enforces nothing, because the prompt said what it should look like and never said what it must not allow.","merge":false,"needs":["a screen inventory","the stages for the entity","a component registry"],"verify":"The first output has every field, every rule and every stage. You spend your time on density, not on missing pieces.","content":"https://ntent.app/r/f/prompt-build-screen","sha256":"332adc0f570a6c0ffba3767fa9827bbcaa38c34c65301013fd188d90766dc893"},{"id":"prompt-refine-screen","name":"Refine what came back","kind":"prompt","tier":2,"lanes":["design"],"enforcement":"none","catches":"Rebuilding a whole screen to change its density.","merge":false,"verify":"One targeted change lands without disturbing the rest of the screen.","content":"https://ntent.app/r/f/prompt-refine-screen","sha256":"072f18eb0e977b4c8c8ff283ceea01c88f923429e11854deda2d4e01a66c1e73"},{"id":"prompt-validate-screen","name":"Check a screen against the model","kind":"prompt","tier":2,"lanes":["design","product"],"enforcement":"none","catches":"Finding out at the client walkthrough that a rule was never enforced anywhere.","merge":false,"verify":"You get a list of gaps, or a clean answer, before anyone else looks at it.","content":"https://ntent.app/r/f/prompt-validate-screen","sha256":"4014d3e2d88486b8b8b670bfcad04a6edc34bdda3343138f0f89d0be30d1788b"},{"id":"prompt-reuse-component","name":"Stop the assistant rebuilding what exists","kind":"prompt","tier":2,"lanes":["design","engineering"],"enforcement":"none","catches":"A 4th version of the same card, built confidently and at speed.","merge":false,"verify":"The new screen imports the existing component instead of defining a new one.","content":"https://ntent.app/r/f/prompt-reuse-component","sha256":"c2f764c4aaa2cdd0d529c730cbfd12e735d1f42da6b461c3c5ea0c3f37bc67ee"},{"id":"prompt-open-questions","name":"Find what is not decided yet","kind":"prompt","tier":2,"lanes":["product","design"],"enforcement":"none","catches":"Designing around an assumption, which is the failure that costs a rebuild rather than an edit.","merge":false,"verify":"The areas you must mark TBD are named before you start, not after.","content":"https://ntent.app/r/f/prompt-open-questions","sha256":"d16ce4237f36843173948a5b51c72df940a812c27f074ec7b935149a109a8951"},{"id":"prompt-coverage","name":"Ask where the gaps are","kind":"prompt","tier":2,"lanes":["product","design","engineering"],"enforcement":"none","catches":"\"Mostly done\" as a status update.","merge":false,"needs":["contract.json","the coverage check"],"verify":"You can name the 3 gaps and the one journey with no UI at all.","content":"https://ntent.app/r/f/prompt-coverage","sha256":"dbcabb553700d50541e9ecf840a96f0a981e14770101234413d48cc50fdc5107"}]}