{"_id":"@ascendenceai/cortena-extensions-conformance","_rev":"4-cc9593cbbf6856625ef89c6b2905e762","name":"@ascendenceai/cortena-extensions-conformance","dist-tags":{"latest":"0.4.0"},"versions":{"0.1.0":{"name":"@ascendenceai/cortena-extensions-conformance","version":"0.1.0","license":"SEE LICENSE IN LICENSE","_id":"@ascendenceai/cortena-extensions-conformance@0.1.0","maintainers":[{"name":"amit_ascendence","email":"connect@mindmentors.net"}],"homepage":"https://github.com/Ascendence-AI-Technology-Pvt-Ltd/cortena-extensions#readme","bugs":{"url":"https://github.com/Ascendence-AI-Technology-Pvt-Ltd/cortena-extensions/issues"},"bin":{"cortena-extension-audit":"dist/audit/cli.js"},"dist":{"shasum":"2d8b3d66888740604002ad7fa1f2a48602c588ee","tarball":"https://registry.npmjs.org/@ascendenceai/cortena-extensions-conformance/-/cortena-extensions-conformance-0.1.0.tgz","fileCount":174,"integrity":"sha512-Qi3JMEEoNaPJ8Hli18BKcvXhrByzCfwRYsQ9haFOe4ZxFzBHckDuNlnFFRxRoT8a4nFXjwbB1CvtZ1y8n97iWQ==","signatures":[{"sig":"MEYCIQChClAX8Urev0v/J9J8+lwTQcbtOcBvMwqa6GkhJ69rJgIhANulWyyuqpJIeVo4ZEFro4cFkQAqAGAXZDCN/0NU4++3","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":375823},"main":"dist/index.js","type":"module","_from":"file:ascendenceai-cortena-extensions-conformance-0.1.0.tgz","types":"dist/index.d.ts","engines":{"node":">=22"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./audit":{"types":"./dist/audit/index.d.ts","default":"./dist/audit/index.js"},"./package.json":"./package.json"},"scripts":{"lint":"tsc --noEmit -p tsconfig.test.json","test":"vitest run","build":"tsc","typecheck":"tsc --noEmit -p tsconfig.test.json"},"_npmUser":{"name":"amit_ascendence","email":"connect@mindmentors.net"},"_resolved":"/private/var/folders/yz/wdclk8jx2l3c4bkzs3wg_0z80000gp/T/7712a0fd829e2a547ef3154ed7bda9ef/ascendenceai-cortena-extensions-conformance-0.1.0.tgz","_integrity":"sha512-Qi3JMEEoNaPJ8Hli18BKcvXhrByzCfwRYsQ9haFOe4ZxFzBHckDuNlnFFRxRoT8a4nFXjwbB1CvtZ1y8n97iWQ==","repository":{"url":"git+https://github.com/Ascendence-AI-Technology-Pvt-Ltd/cortena-extensions.git","type":"git","directory":"packages/conformance"},"_npmVersion":"11.9.0","description":"cortena-extension-audit — the protocol audit of §32, and the platform conformance suite every Cortena extension imports.","directories":{},"_nodeVersion":"25.6.1","dependencies":{"ajv":"^8.17.1","yaml":"^2.6.0","ajv-formats":"^3.0.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.0.0","typescript":"^5.7.0","@types/node":"^22.0.0"},"_npmOperationalInternal":{"tmp":"tmp/cortena-extensions-conformance_0.1.0_1788785630983_0.52385932146702","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@ascendenceai/cortena-extensions-conformance","version":"0.2.0","license":"SEE LICENSE IN LICENSE","_id":"@ascendenceai/cortena-extensions-conformance@0.2.0","maintainers":[{"name":"amit_ascendence","email":"connect@mindmentors.net"}],"homepage":"https://github.com/Ascendence-AI-Technology-Pvt-Ltd/cortena-extensions#readme","bugs":{"url":"https://github.com/Ascendence-AI-Technology-Pvt-Ltd/cortena-extensions/issues"},"bin":{"cortena-extension-audit":"dist/audit/cli.js"},"dist":{"shasum":"39b19317b1e86db793bb2776119849ad3386f235","tarball":"https://registry.npmjs.org/@ascendenceai/cortena-extensions-conformance/-/cortena-extensions-conformance-0.2.0.tgz","fileCount":191,"integrity":"sha512-9rPqZuRmK8InC977orP7/Z1x8acmejdsMC+hVO87f2pADBVs7yR1fB6ZST35htB5xBq/1Dm2+yIZwiRN6ycviA==","signatures":[{"sig":"MEUCIQCggCMrRZaWhylJqI5n2toinuof3W8nzODiPNUIiv50zgIgO1ndICkoXSJf76D81lNEEdk2T/xfH2ZItcCXQcks94s=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":638410},"main":"dist/index.js","type":"module","_from":"file:ascendenceai-cortena-extensions-conformance-0.2.0.tgz","types":"dist/index.d.ts","engines":{"node":">=22"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./audit":{"types":"./dist/audit/index.d.ts","default":"./dist/audit/index.js"},"./package.json":"./package.json"},"scripts":{"lint":"tsc --noEmit -p tsconfig.test.json","test":"vitest run","build":"tsc && node scripts/write-build-info.mjs","typecheck":"tsc --noEmit -p tsconfig.test.json"},"_npmUser":{"name":"amit_ascendence","email":"connect@mindmentors.net"},"_resolved":"/private/var/folders/yz/wdclk8jx2l3c4bkzs3wg_0z80000gp/T/43aa92b0987dfd27c1be8869246faacd/ascendenceai-cortena-extensions-conformance-0.2.0.tgz","_integrity":"sha512-9rPqZuRmK8InC977orP7/Z1x8acmejdsMC+hVO87f2pADBVs7yR1fB6ZST35htB5xBq/1Dm2+yIZwiRN6ycviA==","repository":{"url":"git+https://github.com/Ascendence-AI-Technology-Pvt-Ltd/cortena-extensions.git","type":"git","directory":"packages/conformance"},"_npmVersion":"11.9.0","description":"cortena-extension-audit — the protocol audit of §32, and the platform conformance suite every Cortena extension imports.","directories":{},"_nodeVersion":"25.6.1","dependencies":{"ajv":"^8.17.1","yaml":"^2.6.0","ajv-formats":"^3.0.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.0.0","typescript":"^5.7.0","@types/node":"^22.0.0"},"_npmOperationalInternal":{"tmp":"tmp/cortena-extensions-conformance_0.2.0_1789364276393_0.12148432039715473","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@ascendenceai/cortena-extensions-conformance","version":"0.3.0","license":"SEE LICENSE IN LICENSE","_id":"@ascendenceai/cortena-extensions-conformance@0.3.0","maintainers":[{"name":"amit_ascendence","email":"connect@mindmentors.net"}],"homepage":"https://github.com/Ascendence-AI-Technology-Pvt-Ltd/cortena-extensions#readme","bugs":{"url":"https://github.com/Ascendence-AI-Technology-Pvt-Ltd/cortena-extensions/issues"},"bin":{"cortena-extension-audit":"dist/audit/cli.js"},"dist":{"shasum":"a69920d2b7964fe61cf429eebb6abda44230cba6","tarball":"https://registry.npmjs.org/@ascendenceai/cortena-extensions-conformance/-/cortena-extensions-conformance-0.3.0.tgz","fileCount":191,"integrity":"sha512-rfNUArqf2S8yHjcOcKgtz6smdKqMSqUWeBn0be/8bzDJ3qunXQKjnCt8m2BG4TXhwEeGoYywkQth4zIX3G6Uww==","signatures":[{"sig":"MEYCIQCKHzVuJBSVehdSEVcNjlbhOeEaG2dTS2gtngB+JQ423QIhAMBmZsMkDlOFU9xFunqVnzzWKKQxcDjmHFBOkmzqNlVx","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":677315},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=22"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./audit":{"types":"./dist/audit/index.d.ts","default":"./dist/audit/index.js"},"./package.json":"./package.json"},"gitHead":"5b4413479574739abf1ec00917fda80b8877d2b1","scripts":{"lint":"tsc --noEmit -p tsconfig.test.json","test":"vitest run","build":"tsc && node scripts/write-build-info.mjs","typecheck":"tsc --noEmit -p tsconfig.test.json","prepublishOnly":"npm run build"},"_npmUser":{"name":"amit_ascendence","email":"connect@mindmentors.net"},"repository":{"url":"git+https://github.com/Ascendence-AI-Technology-Pvt-Ltd/cortena-extensions.git","type":"git","directory":"packages/conformance"},"_npmVersion":"11.9.0","description":"cortena-extension-audit — the protocol audit of §32, and the platform conformance suite every Cortena extension imports.","directories":{},"_nodeVersion":"25.6.1","dependencies":{"ajv":"^8.17.1","yaml":"^2.6.0","ajv-formats":"^3.0.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.0.0","typescript":"^5.7.0","@types/node":"^22.0.0"},"_npmOperationalInternal":{"tmp":"tmp/cortena-extensions-conformance_0.3.0_1789393487131_0.9666668659703181","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"_id":"@ascendenceai/cortena-extensions-conformance@0.4.0","bin":{"cortena-extension-audit":"dist/audit/cli.js"},"bugs":{"url":"https://github.com/Ascendence-AI-Technology-Pvt-Ltd/cortena-extensions/issues"},"dist":{"shasum":"3e389039140394d3c3acfcf0e5728be17500569f","tarball":"https://registry.npmjs.org/@ascendenceai/cortena-extensions-conformance/-/cortena-extensions-conformance-0.4.0.tgz","fileCount":194,"integrity":"sha512-sZAvFdVPXwdce4cIcasCyDfhjb+ptGmA/IFZBvHNnLlkxae4PPjCVTsepYdHUfsHQ91jl5f71w79IfmaXZwEUg==","signatures":[{"sig":"MEQCIGwm/F7AvkVNWPNvL42v+bXEMyrrq4+O2T0kVEAsDIjjAiB5ZmWgM2gsCafGixoL3fH5NXY2rDnf4GSzu3f5q5xgNw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDjojG+UPil8XrIR3rD+UQVwCcyrv10NCBtkxPX17GoiQIgNzE185lT67nNcK/pkpposo9rnvVOU2g2gTlDvwMLNAU="}],"unpackedSize":712596},"main":"dist/index.js","name":"@ascendenceai/cortena-extensions-conformance","type":"module","_from":"file:ascendenceai-cortena-extensions-conformance-0.4.0.tgz","types":"dist/index.d.ts","engines":{"node":">=22"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./audit":{"types":"./dist/audit/index.d.ts","default":"./dist/audit/index.js"},"./package.json":"./package.json"},"license":"SEE LICENSE IN LICENSE","scripts":{"lint":"tsc --noEmit -p tsconfig.test.json","test":"vitest run","build":"tsc && node scripts/write-build-info.mjs","typecheck":"tsc --noEmit -p tsconfig.test.json"},"version":"0.4.0","_npmUser":{"name":"amit_ascendence","email":"connect@mindmentors.net"},"homepage":"https://github.com/Ascendence-AI-Technology-Pvt-Ltd/cortena-extensions#readme","_resolved":"/private/var/folders/yz/wdclk8jx2l3c4bkzs3wg_0z80000gp/T/23cde8f133e7e120b8ac2307134e79fd/ascendenceai-cortena-extensions-conformance-0.4.0.tgz","_integrity":"sha512-sZAvFdVPXwdce4cIcasCyDfhjb+ptGmA/IFZBvHNnLlkxae4PPjCVTsepYdHUfsHQ91jl5f71w79IfmaXZwEUg==","repository":{"url":"git+https://github.com/Ascendence-AI-Technology-Pvt-Ltd/cortena-extensions.git","type":"git","directory":"packages/conformance"},"_npmVersion":"11.9.0","description":"cortena-extension-audit — the protocol audit of §32, and the platform conformance suite every Cortena extension imports.","directories":{},"maintainers":[{"name":"amit_ascendence","email":"connect@mindmentors.net"}],"_nodeVersion":"25.6.1","dependencies":{"ajv":"^8.17.1","yaml":"^2.6.0","ajv-formats":"^3.0.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.0.0","typescript":"^5.7.0","@types/node":"^22.0.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/cortena-extensions-conformance_0.4.0_1790491498137_0.648009196728978"}}},"time":{"created":"2026-09-07T12:53:50.813Z","modified":"2026-09-27T06:44:58.451Z","0.1.0":"2026-09-07T12:53:51.173Z","0.2.0":"2026-09-14T05:37:56.515Z","0.3.0":"2026-09-14T13:44:47.258Z","0.4.0":"2026-09-27T06:44:58.286Z"},"bugs":{"url":"https://github.com/Ascendence-AI-Technology-Pvt-Ltd/cortena-extensions/issues"},"license":"SEE LICENSE IN LICENSE","homepage":"https://github.com/Ascendence-AI-Technology-Pvt-Ltd/cortena-extensions#readme","repository":{"url":"git+https://github.com/Ascendence-AI-Technology-Pvt-Ltd/cortena-extensions.git","type":"git","directory":"packages/conformance"},"description":"cortena-extension-audit — the protocol audit of §32, and the platform conformance suite every Cortena extension imports.","maintainers":[{"name":"amit_ascendence","email":"connect@mindmentors.net"}],"readme":"# `@ascendenceai/cortena-extensions-conformance`\n\n`cortena-extension-audit` — the protocol audit of §34 of *How to Create a\nCortena Extension*. One row per rule id, `P-01` to `P-44`, checked mechanically\nwherever §34.1 says a criterion can be checked without judgement.\n\n> The audit is not a review. A row passes or it does not. If a criterion turns\n> out to need an opinion, that is a defect in the how-to — fix the criterion\n> there rather than deciding it per extension, because a criterion decided per\n> extension is how the fleet drifted in the first place. (§34.4)\n\n## Usage\n\n```sh\n# one extension, from the repository root where design-baseline.json lives\npnpm exec cortena-extension-audit extensions/tasks\n\n# the machine report the Assure sync reads, and the markdown a reviewer reads\npnpm exec cortena-extension-audit extensions/tasks --json out/audit.json --markdown out/audit.md\n\n# every extension in the workspace\npnpm exec cortena-extension-audit --all\n```\n\nBuild the extension first. The rows that read the served OpenAPI document, the\nresponse headers, the MCP challenge and the built MCP App documents start the\nextension's own compiled entry (`node functions/dist/index.js`) on a free port,\nwith `PORT` set. Without a build those rows report **not built**, which is a\nfailure — never a pass.\n\n### `cortena.audit.json` — how the extension says it is started\n\nAn extension whose compiled `index.js` does not listen — or whose config\nrefuses to boot without variables the audit cannot guess — declares how to\nstart it in `cortena.audit.json` beside `docs/` (TASKS-39). Tasks' is the\nreference:\n\n```json\n{\n  \"build\": \"pnpm --dir functions build\",\n  \"startCmd\": \"node dist/prod-server.js\",\n  \"cwd\": \"functions\",\n  \"port\": 0,\n  \"healthPath\": \"/health\",\n  \"env\": { \"AUTH_SERVICE_URL\": \"http://127.0.0.1:9\", \"TASKS_DELEGATION_SECRET\": \"cortena-audit-fixture-not-a-secret\" }\n}\n```\n\nThe file is schema-validated (`AUDIT_MANIFEST_SCHEMA` in `src/audit/manifest.ts`;\nunknown keys are refused). `build` is named in the failure when the app does not\nstart and is never run. `port: 0` means a free port. `env` holds fixture values\nonly, handed to the child as overrides and never copied from your shell. The\nfile is committed, so a name containing `SECRET`, `PASSWORD` or `TOKEN`, or\nending `KEY`, must carry the literal `cortena-audit-fixture-not-a-secret`; any\nother value and the audit refuses the whole file, prints the variable's name\n(never its value) and exits 2. An explicit `--start-cmd` or `--port` wins over\nthe file.\n\n| Option | |\n| --- | --- |\n| `--all` | audit every extension under `extensions/` |\n| `--json <path>` | the machine report (§23.7) |\n| `--markdown`, `--md <path>` | the pull-request table |\n| `--repo-root <path>` | where `design-baseline.json` lives; defaults to the cwd |\n| `--enterprise-root <path>` | a CortenaEnterprise checkout, so the chart rows (P-31, P-32) are checked rather than deferred — without it they are `deferred`, which fails the run |\n| `--design-gate-cmd <cmd>` | how to run `cortena-design-gate` |\n| `--start-cmd <cmd>` | how to start the built app. It is run as an argv, never through a shell, and the process is given an allowlisted environment — see [What the audited application is given](#what-the-audited-application-is-given) |\n| `--port <n>` | a fixed port instead of a free one |\n| `--start-timeout <ms>` | how long to wait for `/health` (default 30000) |\n| `--no-server` | do not start the app; the rows that need it report `not built` |\n| `--list-rules` | print the rule table and exit |\n\n## The exit-code contract\n\n| Code | Meaning |\n| --- | --- |\n| `0` | every row passed. The manual rows still need a named sign-off — the audit records them, it does not decide them |\n| `1` | at least one row is `fail`, `not built`, or `deferred` on a run that was not given `--enterprise-root`. A check that did not run is never a pass, so a missing build — or a deployment repository the run could not read — fails the audit rather than shrinking it |\n| `2` | the tool could not run at all: a bad option, no extension at that path, an invalid or secret-carrying `cortena.audit.json`, or `--strict` on a build older than the rules beside it |\n\nCI, the reusable extension workflow's `audit` job and the catalogue-registration\ngate all read the same code, so “no green audit, no catalogue row, no Apps tile”\n(§34.4) is one condition rather than three interpretations of one.\n\n## The five statuses\n\n| Status | |\n| --- | --- |\n| `pass` | the criterion held |\n| `fail` | it did not, and the row names the file and the reason |\n| `manual` | §34.3 — the row needs judgement, and carries the checklist line its named owner signs off against. **Never** reported as a pass |\n| `not built` | the row needed the built app or a built artefact and did not get one. Counts as a failure |\n| `deferred` | the row reads a repository this run was not pointed at. Counts as a failure unless `--enterprise-root` was given |\n\n`deferred` is the status for a row whose input is in another repository: the\nchart, the values block, the HTTPRoute and the HealthCheckPolicy live in\n`CortenaEnterprise/k8s` (§4), so P-31 and P-32 answer `deferred` unless\n`--enterprise-root` points at a checkout. It is not `manual` — nobody signs it\noff, and it is not waiting on judgement — and it is not a pass: an audit that\ncould not read the deployment artefacts has not checked them, so the run is red\nuntil it is given the repository. Point `--enterprise-root` at a *wrong* path\nand the rows fail outright rather than deferring, because a typo must not turn a\ndeploy row green.\n\nAn extension directory with no `web/` or no `functions/src/` fails the rows that\nread them for the same reason: an empty input is not a clean one.\n\n## The routes that are the platform's, not the extension's\n\nThree rows read the router — P-17 (every mutating route has a tool), P-18 (every\nroute is in `docs/openapi.yaml`) and P-26 (every write has an actor in scope) —\nand they read **one** list of the routes that are not capabilities\n(`PLATFORM_ROUTES` in `src/audit/rules/util.ts`). One list rather than three,\nbecause three rows disagreeing about what a platform route is would be a route\nexempt from the document but not from tool parity, failing an extension for the\nabsence of a tool that could only exist if the document carried the route it is\nexempt from carrying.\n\n| On the router | Why it is not a capability |\n| --- | --- |\n| `GET /health`, `GET /openapi.json`, `GET /config.json` | the §5 and §16.2 contract |\n| `/.well-known/*`, `/oauth/*` | the §15 discovery documents |\n| the SPA catch-all | not an API at all |\n| `POST`, `GET`, `DELETE` `/mcp` | the Streamable HTTP MCP transport §16 requires. It is JSON-RPC over one path — a session, not a capability. There is no catalogue row to write for it (P-18), no *second* tool to generate for it since it is the surface the tools are served on (P-17), and no write of its own to attribute since it is where the actor is resolved and the writes it carries are the tools' own (P-26). The exemption is the **exact** path: `/mcp/sessions` is an ordinary route and is held to all three rows |\n\nAnd one row of the catalogue is allowed to be empty. A 204, a 205 and a 304\ncarry no body by the definition of the status: the shared router `.end()`s a 204\nrather than sending `null` under a JSON content-type, and the generator writes\nthe 204 response with a description and no `content`. So P-19 requires a success\nstatus to be **declared** on every operation, and requires an\n`application/json` schema under it only where the status says there is something\nto parse.\n\n## Where an extension's identity comes from\n\n`x-cortena.id` in `docs/openapi.yaml`, and nowhere else (§16.2.1). It\nis the tool prefix, the operationIds, the skill prefix, the `service.name`\nsuffix, the AgentTemplate slug, the chart name and the catalogue row's id — one\nstring, declared once. The directory is a place on somebody's disk:\n`extensions/Assure` holds the extension whose id is `assure`, and every row that\nmeans the identity reads the id.\n\n| Row | What it compares |\n| --- | --- |\n| `P-24` | the AgentTemplate `slug` is the id |\n| `P-32` | the chart is `ext-<id>`, under `k8s/helm/extensions/<id>`, with an `<id>:` block in each values file |\n| `P-40` | `x-cortena.id` and the directory name are the same word, compared without case — this is the row that keeps the two from drifting |\n| `P-41` | `service.name` is `ext-<id>` |\n| `P-44` | the skill id is `skill.<id>.<topic>`, and `capabilities[]` names ids under `<id>.` |\n\nWith no document there is no id to read, so the rules fall back to the\nlower-cased directory name — which is the id the extension will have to declare,\nsince P-40 compares the two without case.\n\n## What the audited application is given\n\nThe rows that read the running app start the extension's own compiled entry, and\nthat entry is the one piece of code in the run nobody has vouched for. It is\ngiven an allowlist and nothing else — not the cloud credentials in the\ndeveloper's shell, not CI's deploy key:\n\n`PATH`, `HOME`, `TMPDIR`, `LANG`, `NODE_ENV`, `PORT`; `CORTENA_ENVIRONMENT`,\n`CORTENA_SERVICE_VERSION`, `CORTENA_SERVICE_NAME`; anything under\n`CORTENA_PUBLIC_` or `CORTENA_EXT_` (§21.1); and **`<NAME>_DATABASE_URL`**,\nwhere `<NAME>` is the extension's own id upper-cased — `assure` reads\n`ASSURE_DATABASE_URL`, `field-service` reads `FIELD_SERVICE_DATABASE_URL`.\n\nA production entry that exits when it has no database is doing the right thing,\nand without that last name it could not be started by the audit at all: every\nrow that reads the running app reported `not built` on an extension that runs.\nThe name is built from the id rather than matched by pattern, so one extension's\naudit cannot be handed another's database, and a bare `DATABASE_URL` — the one a\ndeveloper's shell is likeliest to hold — does not cross. A name ending `_TOKEN`,\n`_SECRET`, `_PASSWORD` or `_KEY` never crosses, whatever else says yes, and\nwhatever the child prints back is redacted before it reaches the report.\n\n## Coverage\n\n36 of the 43 live rules are checked mechanically; 7 are the rows §34.3 leaves to\na named owner (`P-08`, `P-13`, `P-20`, `P-23`, `P-33`, `P-34`, `P-35`). `P-15`\nis retired — it covered the shape of `cortena.plugin.json`, and its replacement\n`P-40` fails an extension that still ships one. `pnpm exec\ncortena-extension-audit --list-rules` prints the table.\n\n## The skills folder (P-44)\n\nP-44 reads `extensions/<name>/skills/`. Every extension ships **exactly one**\ntop-level skill, `skills/overview/SKILL.md` with the id\n`skill.<id>.overview`, and everything else beneath it as a\n`references/*.md` — since SKILLS-D8 a skill row is a file in every agent's\nworkspace and an extension's folder is derived from the extension segment of\nits id, so a second top-level skill maps onto the same `skills/<extension>/`\nfolder and one of the two is simply absent everywhere. The row checks what a\nmachine can: the front matter parses; the id is `skill.<id>.<topic>` under\n*this* extension's prefix — the id being `x-cortena.id` rather than the folder,\nso `extensions/Assure` wants `skill.assure.overview`, which is also the only\nspelling the lower-case id pattern allows; the summary is 160 characters or\nfewer; the body is under 12 kB and each `references/*.md` under 32 kB; and every id in\n`capabilities[]` belongs to this extension and matches an operation in\n`docs/openapi.yaml` — by `operationId`, `x-cortena-tool` or `x-cortena-id`,\ncompared with the separators flattened, since a catalogue id is dotted and a\ntool name cannot be.\n\nThe one-per-extension rule itself is enforced where the rows are written, in\nEngram's ingest (`ONE_PER_EXTENSION` and `ID_DRIFT` in\n`extensions/engram/functions/src/ingest/skills.ts`). This audit does not check\nit yet; when it does, the two halves move together the way the parsing already\ndoes.\n\nWith no document there is nothing to resolve against, so the row reports\n`manual` and names the rows that would decide it (P-18, P-40). It is not a pass:\na capability id nobody could check is a describe pointer that may never appear.\n\nThe parsing and the limits are a **deliberate copy** of Engram's ingest\n(`extensions/engram/functions/src/ingest/skills.ts`), kept in\n`src/audit/skills.ts` so the audit does not depend on an extension's server\ncode. The rules belong to §17 of the how-to; when it changes, both move.\n\n## What the running app is asked, and why it is safe\n\nStarting the built entry is not only about reading the served document. Three\nrows are assertions about behaviour that no amount of source-reading can settle:\n`/health` answering 200 (P-01), `/mcp` failing closed with the RFC 9728\nchallenge (P-16), and — since EXTBP-26 — **P-22's error envelope**.\n\nP-22 sends two requests it expects to fail: a malformed JSON body, and a body\ncarrying none of the declared fields, both to the first mutating path in the\nserved document. Whatever comes back must be the §16.6 envelope —\n`{ ok, status, code, message }`, in JSON, with the body's `status` equal to the\nHTTP one, no stack trace, no HTML page and no database driver message. A 400\nmust also name a field, because a 400 that says `Invalid input` costs the agent\na whole round-trip and a guess.\n\nThe status itself is never asserted. A conformant extension refuses the probe\nwith a 401 before it ever reads the body, and that refusal is correct — the rule\nis about the shape of the refusal.\n\n**The probes carry no credentials**, which is what makes them safe to point at a\nreal deployment: P-01 puts every `/v1` route behind `authenticate`, so nothing\nis written. The malformed-body probe is the one that finds the defect:\n`express.json()` throws *before* the router is reached, and Express skips a\n`Router` once an error is in flight, so an extension with no terminal error\nhandler answers it with Express's default HTML page — stack trace included —\nwhich the broker can only wrap in a generic `upstream_error`.\n\n`@ascendenceai/cortena-extensions-shared` mounts both handlers for you; see its README.\n\n## Which build produced a report\n\nEvery report carries the build that decided it: the version, the commit\n`packages/conformance` was built from (`+dirty` when the working tree was), the\nbuild time, and the newest mtime under the `dist/` that ran. `report.build` in\nthe JSON, a `Tool build:` line under the markdown heading, and one line on the\nterminal before the first table.\n\nIt is there because a semver does not move when a rule does. A report produced\nby a `dist/` built before the previous ticket's rule fixes landed was\nindistinguishable from a current one, and on 2026-09-14 seven extensions were\naudited, written up and ticketed from a sixteen-hour-old bin; three findings had\nto be withdrawn and three reports moved rows on a re-run at the same commit.\n\n`dist/` is gitignored fleet-wide, so it outlives a branch switch that rewrote\nevery rule under `src/`. When a file under `src/audit` is newer than the bin,\nthe run prints a banner to stderr and still produces its table — the audit is a\ngate other work waits on, and a stale build must not become a second way for it\nto fail. `--strict` turns that into a refusal: exit 2, no table. That is the\nflag for CI, which is the caller that must never publish a stale report at all —\nand `.github/workflows/extension-ci.yml` passes it.\n\n## What a rule does when the extension satisfies it through a package\n\nThe protocol keeps moving behaviour into shared components, and a rule that\ngreps only the extension's own files gets *less* accurate every time an\nextension adopts one. `<AppShell>` draws the bottom-right cluster;\n`SessionGuard` owns the cross-tab channel and the rotation schedule;\n`registry.serve()` mounts `/health` and `/openapi.json`; the shared router\nflattens zod issues into `fieldErrors`. None of it is written in the extension,\nwhich is the point.\n\n`src/audit/shared.ts` is the one place that answers \"is it in the package they\nmount\", so fourteen rules do not each grow their own half-right version of the\nquestion. `mountedFrom(dir, name, package)` wants both the import and the JSX\nelement, so a hand-rolled component of the same name is not the package's — and\nwhere §10.4.2 says a thing must *not* be reimplemented per extension, the rule\nsays so rather than looking for it twice.\n\nIt follows one hop through a local barrel: `web/src/ui.ts` holding\n`export { SessionGuard } from 'cortena-ui'`, imported as\n`import { SessionGuard } from './ui'`, is the package's component. One hop, and\nonly through a module inside the app — a barrel that re-exports the name from\nsomewhere else in `web/src` is still refused, or the first barrel in an app\nwould launder every local component through it.\n\n## The four rows that read the frame instead of the pieces\n\ncortena-ui 1.15.0 ships `CortenaApp` from `cortena-ui/app` — the composition\nthat *is* the frame: the sign-in screen, **one** `SessionGuard` above both the\nshell and the agent pop-up, `AppShell`, the pop-up behind a lazy `AgentMount`,\nthe standard Profile / Settings / Permissions pages and the tab title. It is one\nmount, and an extension that adopts it writes none of the pieces.\n\nFour rows used to grep for those pieces, so the first extension to adopt the\ncomposition scored 27 where the hand-built tree it replaced scored 30 — marked\ndown three rows for doing the thing the protocol moved it to. From 0.3.0 each of\nthem recognises `mountedFrom(webSrc, 'CortenaApp', /^cortena-ui\\/app$/)` and\nasks what is genuinely still the extension's (DESIGN-124):\n\n| rule | with a `<CortenaApp>` mounted | still fails when |\n| --- | --- | --- |\n| `P-10` | the composition renders `AppShell`, so the shell clause is satisfied by the frame | the frame carries no `extension={…}` — there is nothing to draw top-left |\n| `P-37` | the frame mounts exactly one guard, and owns its channel, countdown and 75% rotation | a **second** `SessionGuard` is mounted anywhere under `web/src` (the finding names the file and line), the `session` adapter has no `refresh`, or the idle timeout is hardcoded |\n| `P-25` | the pop-up is the `agent` prop, not an element | `agent` is absent, its `slug` is missing, or neither it nor `session` supplies a `getToken`. The getter is read off `session` because that is where the composition takes it from: `CortenaAppAgent` has no token field — the frame calls `createClient({ agentId, baseUrl, getToken })` with the session adapter's own getter |\n| `P-38` | the built `web/dist/index.html` is read — **and only then**, never merely because a `web/dist` exists — where `cortenaFavicon()` from `cortena-design/vite` writes the three links and the three files | a link or a file is missing from the build. With no build it falls back to `web/index.html` and `web/public`, and the row says which it read |\n\nA bare identifier is followed one hop to its declaration, so `agent={AGENT}` with\n`const AGENT = { slug, … }` at module scope reads as the object. That spelling is\nnot a style: a value rebuilt per render gives the chat client a new identity each\ntime and it refetches its session list per render, so the fleet hoists it — and a\nrule that only read the element would have failed the better spelling.\n\nP-38's preference for the build output is gated on the frame being mounted, not\non a `web/dist` being there. `extension-ci.yml` builds the web surface before it\naudits, so the whole fleet is audited on built trees — and a stale dist,\n`copyPublicDir: false`, a custom `publicDir` or any head-rewriting plugin gives\na built document that does not carry the links its source does. Reading it for a\nhand-built extension would fail twelve of them six ways and point the reader at\n`web/dist/favicon.svg`, a gitignored directory.\n\nP-37's \"is there a second guard\" follows an import alias:\n`import { SessionGuard as AuthGuard }` mounted as `<AuthGuard>` is a second\nguard, and a scan for the literal string would walk past it. `mountedFrom` is\ndeliberately NOT widened the same way — fourteen rules read it on the whole\nfleet, so that is a change to the fleet's scores rather than to this row.\n\nExtensions still on the hand-built tree are read exactly as before. A release\nthat teaches the rules a new shape must not move a single score on the old one,\nand `rules.test.ts` and `design-102.test.ts` are what holds that — plus one case\nin `design-124.test.ts` that puts a stale `web/dist/index.html` into the\nhand-built fixture and asserts P-38 still reads `web/public`. The frame's own\npairs are in the same file, against a fixture copied from the generator's\noutput.\n\n## An environment that runs no extensions\n\nP-32 judges every environment under `k8s/envs` that has a `values.yaml`. An\nenvironment says it is out of scope by declaring a top-level `extensions: {}` —\nan explicitly empty map. Silence is not a declaration: a values file carrying no\n`extensions:` map at all used to be skipped, which turned *this extension is\nabsent from an entire environment* into a pass.\n\nThe declaration lives in the file the environment owns rather than in a flag in\nthe extension, because whether an environment runs extensions is a fact about\nthe environment, and it is then true for all fourteen at once rather than\nfourteen times.\n\n## Adding or changing a rule\n\nOne module per rule id in `src/audit/rules/`, wired into the table in\n`src/audit/rules.ts` with its section, title and whether it is automated. A rule\nreturns `verdict(meta, findings)`, and a finding says the file and the reason in\nterms a reader can act on without opening this package — because each failed row\nbecomes one defect titled `<rule id>: <short failure>`, and “make it conformant”\nis the ticket nobody picks up (§34.4).\n\nEvery rule is covered twice by `pnpm test`: `fixtures/passing/` is a conformant\nextension skeleton that must stay green on every automated row, and\n`fixtures/failing/<rule-id>/` is the smallest edit to it that turns exactly that\nrow red. Adding an automated rule without its failing fixture fails the suite.\n","readmeFilename":"README.md"}