@supersuit/hypercontext 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/LICENSE +21 -0
- package/README.md +103 -2
- package/bin/hypercontext.mjs +91 -0
- package/docs/STANDARDS.md +86 -0
- package/package.json +17 -4
- package/src/baseline.mjs +49 -0
- package/src/check.mjs +114 -0
- package/src/find.mjs +157 -0
- package/src/index.mjs +9 -0
- package/src/kinds.mjs +77 -0
- package/src/manifest.mjs +129 -0
- package/src/packages.mjs +175 -0
- package/src/render.mjs +51 -0
- package/src/selfdoc.mjs +56 -0
- package/src/stamp.mjs +96 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
Every kind is now checked by its own package, so `check` reports more findings on the same workspace: a minor version, because it changes what is checked.
|
|
6
|
+
|
|
7
|
+
- `document` is checked and stamped by `@supersuit/document-md`, `checkpoint` by `@supersuit/checkpoint-md`, `hdsop` and `workflow-catalog` by `@supersuit/hdsop-md` (one run covers both, each finding filed under the kind it is about), `recurrent` by `@supersuit/recurrent-md`, `tile-app` by `@supersuit/tile-app-md`. None of the six raises `validator:missing` any more.
|
|
8
|
+
- This package no longer ships manifests: each kind's comes from its package. A kind with no package and no manifest is listed as missing rather than crashing `check`.
|
|
9
|
+
- `recurrent` and `tile-app` are stamped. A recurrent goes through `recurrent-md migrate`, which moves its body labels into the header as it names the standard; a tile app through `tile-app-md stamp`.
|
|
10
|
+
- `stamp` repeats each package's closing count and every record it skipped, with the reason, so a catalog with no header or a headerless document is named in the report rather than lost.
|
|
11
|
+
- A kind whose package run also checks another (hdsop checks the catalog) runs when only the other kind has records.
|
|
12
|
+
|
|
13
|
+
## 0.1.0
|
|
14
|
+
|
|
15
|
+
- The header codec, `versionOf`, `bareStandard` and the byte-preserving `stampStandard` now come from `@supersuit/hypercontext-core`, a dependency at `^0.1.0`; they are re-exported here unchanged.
|
|
16
|
+
- Every manifest an owner ships is checked against core's `validateManifest`: a bad one is `manifest:invalid` (error) in `check`, and `manifests` lists it as invalid, never publishes it, and exits 1.
|
|
17
|
+
- Each kind's package is found by node resolution and then beside this package in the hypercontext repository, so a checkout of that repository needs no `--packages`. `--packages` (or `HYPERCONTEXT_PACKAGES`) now means only that folder.
|
|
18
|
+
- The document manifest's `created` key is `type: string` with a date pattern; `date` is not a type a reader can check.
|
|
19
|
+
|
|
20
|
+
The first version: one check over a whole workspace.
|
|
21
|
+
|
|
22
|
+
- `hypercontext check` finds every record of fifteen kinds, judges the property they share (each names the standard it follows, `self-doc:missing` when not), counts the shared context keys, and runs each kind's own package (self-md, friend-md, group-md, superprojects, superobjective, piece-md, transcript-md). One row per kind. A kind no package checks yet is a warning saying so.
|
|
23
|
+
- `hypercontext stamp` is the backfill: each packaged kind through its own package's command, the rest by one byte-preserving edit. Conversations go through `transcript-md stamp-standard`, which takes its lock and checks each file's hash. Never gives a headerless file a header, never touches a recurrent or tile app, never follows a link.
|
|
24
|
+
- `hypercontext baseline` and `check --baseline`: known debt is tolerated, new debt fails, and the baseline only shrinks unless `--accept`.
|
|
25
|
+
- `hypercontext kinds` lists every kind, its standard and its package.
|
|
26
|
+
- Standards are versioned: a record names `https://hypercontext.wiki/standards/<kind>/v<N>`. The unversioned link is `self-doc:missing` and `stamp` upgrades it; a version the owner does not implement is `self-doc:version`. The implemented versions are read from the manifests the owner ships.
|
|
27
|
+
- Manifests (docs/STANDARDS.md): this package ships them for the six kinds no package owns; `hypercontext manifests --out <dir>` copies every kind's from its owner for a site to publish; `check --online` refuses a standard that does not open (`standard:unreachable`) or whose published manifest is not its owner's (`standard:drift`).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SupersuitUp
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,104 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @supersuit/hypercontext
|
|
2
2
|
|
|
3
|
-
This
|
|
3
|
+
*This package lives in the [hypercontext repository](https://github.com/SupersuitUp/hypercontext/tree/main/packages/hypercontext), with every other record standard; issues and changes go there.*
|
|
4
|
+
|
|
5
|
+
One check over a whole workspace, for every kind of record in it.
|
|
6
|
+
|
|
7
|
+
A hypercontext workspace keeps a life or a company as plain files: a person in `people/<slug>/friend.md`, a project in `superprojects/<slug>/`, a conversation in `meeting-transcripts/<id>/transcript.md`, and so on. Each kind has its own standard and its own package that checks it. This package is the umbrella: it finds every record of every kind, asks each kind's package to check its own, and checks the one property they all share.
|
|
8
|
+
|
|
9
|
+
## The shared property: every record names its standard
|
|
10
|
+
|
|
11
|
+
**A record explains itself to a stranger or a fresh agent with no other context.** It does that by carrying, in its header, the address of the rules it was written to:
|
|
12
|
+
|
|
13
|
+
```yaml
|
|
14
|
+
---
|
|
15
|
+
kind: friend
|
|
16
|
+
standard: "https://hypercontext.wiki/standards/friend/v1"
|
|
17
|
+
id: maya-lin
|
|
18
|
+
...
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The link names a **version**. The page at `/v1` never changes meaning, so the record says which rules it was written to; `/standards/friend` is the latest page, for people. A record without the line, or with the unversioned link written before versions, is not self-documenting however complete the rest of it is: refused as `self-doc:missing`. A version of its own standard that the installed checker does not implement is `self-doc:version`.
|
|
22
|
+
|
|
23
|
+
Every version also has a **manifest**, `/standards/<kind>/v<N>.json`: the same rules as data (required header keys, every code its checker can report with what it means and how to fix it, the exact commands), so an agent can validate a record with nothing installed. The package that owns a kind ships its manifest, and a test there proves the manifest names exactly the codes its lint reports. [docs/STANDARDS.md](docs/STANDARDS.md) is the format. The shared context keys (`id`, `summary`, `read_when`, `as_of`, `keep`) are counted beside it and reported per kind; whether an empty one is a defect is each kind's own package's call.
|
|
24
|
+
|
|
25
|
+
This package defines no record type. A kind's shape belongs to its package. The list below only says where each kind lives and whom to ask.
|
|
26
|
+
|
|
27
|
+
| kind | standard | checked by |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| self | /standards/self | @supersuit/self-md |
|
|
30
|
+
| friend | /standards/friend | @supersuit/friend-md |
|
|
31
|
+
| group | /standards/group | @supersuit/group-md |
|
|
32
|
+
| superproject, state, atomic-action-map | /standards/superproject, /state, /atomic-action-map | @supersuit/superprojects |
|
|
33
|
+
| superobjective | /standards/superobjective | @supersuit/superobjective |
|
|
34
|
+
| piece | /standards/piece | @supersuit/piece-md |
|
|
35
|
+
| conversation | /standards/conversation | @supersuit/transcript-md |
|
|
36
|
+
| document | /standards/document | @supersuit/document-md |
|
|
37
|
+
| checkpoint | /standards/checkpoint | @supersuit/checkpoint-md |
|
|
38
|
+
| hdsop, workflow-catalog | /standards/hdsop, /workflow-catalog | @supersuit/hdsop-md |
|
|
39
|
+
| recurrent | /standards/recurrent | @supersuit/recurrent-md |
|
|
40
|
+
| tile-app | /standards/tile-app | @supersuit/tile-app-md |
|
|
41
|
+
|
|
42
|
+
Every kind has a package. A kind added later without one would still be checked for the shared property, with a `validator:missing` warning saying nothing checks its shape.
|
|
43
|
+
|
|
44
|
+
Every standard lives at `https://hypercontext.wiki/standards/<kind>/v<N>`. `hypercontext kinds` prints the list.
|
|
45
|
+
|
|
46
|
+
## Use
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
npx @supersuit/hypercontext check . # every kind, one row each; exit 1 on any error
|
|
50
|
+
npx @supersuit/hypercontext stamp . # what the backfill would add (dry run)
|
|
51
|
+
npx @supersuit/hypercontext stamp . --apply # add the missing standard: to existing records, or upgrade an unversioned one
|
|
52
|
+
npx @supersuit/hypercontext check . --online # also prove every standard the records name opens, onto its owner's manifest
|
|
53
|
+
npx @supersuit/hypercontext manifests --out static/standards # every kind's manifest, copied from its owner, for a site to publish
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Airtight, both ways
|
|
57
|
+
|
|
58
|
+
`check --online` fetches every distinct standard the workspace's records name. A page that does not answer is `standard:unreachable`; a page whose `.json` manifest differs from the one the installed owner ships is `standard:drift`. So every link a record carries opens, onto the rules the checker actually enforced.
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
kind records names standard context validated by
|
|
62
|
+
self 1 1/1 1/1 @supersuit/self-md@0.2.0
|
|
63
|
+
friend 833 833/833 5/833 @supersuit/friend-md@0.3.0
|
|
64
|
+
document 663 472/663 663/663 @supersuit/document-md@0.1.0
|
|
65
|
+
...
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### The backfill goes through each kind's own package
|
|
69
|
+
|
|
70
|
+
`stamp` never writes a packaged kind's records itself. It runs the backfill that kind's package ships (`friend-md stamp`, `group-md migrate`, `superprojects migrate --stamp-standard`, `self-md stamp-standard`, `transcript-md stamp-standard`, `piece-md stamp`, `document-md stamp`, `checkpoint-md stamp`, `hdsop-md stamp`, `recurrent-md migrate`, `tile-app-md stamp`), because the package knows its own key order and everything else its writers keep. The report repeats each package's closing count and every record it skipped, with its reason (a document with no header, a workflow catalog with none). A kind with no package would be stamped here, by one byte-preserving edit: the line goes directly after `kind:` (or first), or an unversioned link of the same kind is upgraded where it stands, and every other byte of the file, line endings included, is unchanged. A file with no header is never given one; it is reported, because a record with no header needs writing, not stamping. A file that already names a standard, right or wrong, is left alone and the wrong one is reported.
|
|
71
|
+
|
|
72
|
+
A recurrent is stamped by `recurrent-md migrate`, which moves its bold body labels into the header in the same edit, so no field is ever kept twice. A tile app is stamped by `tile-app-md stamp`, a textual edit to `app.json` that adds `"standard"` as its first key. A kind marked `stampable: false` (none is, since 0.2.0) is never touched, and the report says why.
|
|
73
|
+
|
|
74
|
+
Linked files are never records here. A document symlinked in from another repository belongs to that repository; counting it would count it twice, and stamping it would write into a tree this check was never pointed at.
|
|
75
|
+
|
|
76
|
+
### The ratchet
|
|
77
|
+
|
|
78
|
+
A workspace adopting the standard holds thousands of records written before it, and a check that stays red until all of them are fixed is a check nobody runs. So:
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
hypercontext baseline . --write hypercontext-baseline.json # today's errors become known debt
|
|
82
|
+
hypercontext check . --baseline hypercontext-baseline.json # fails only on an error the baseline does not hold
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Rewriting the baseline drops what was fixed and refuses anything new unless you pass `--accept`, so it only ever shrinks by accident.
|
|
86
|
+
|
|
87
|
+
### Where the packages come from
|
|
88
|
+
|
|
89
|
+
Each kind's package is found by normal resolution, then beside this package in the hypercontext repository (`packages/<folder>`), so a checkout of that repository needs no flag. `--packages <dir>` (or `HYPERCONTEXT_PACKAGES`) names a folder of checkouts, one per folder name, to use instead, and then only that folder is looked in. A package that cannot be found is a warning naming it, never a silent pass; a package that runs and prints nothing usable is an error.
|
|
90
|
+
|
|
91
|
+
Every manifest an owner ships is checked against `@supersuit/hypercontext-core`'s `validateManifest`. A bad one is `manifest:invalid` (error) in `check`, and `manifests` lists it as invalid and never publishes it.
|
|
92
|
+
|
|
93
|
+
## Exit codes
|
|
94
|
+
|
|
95
|
+
0 clean, 1 findings (or, with `--baseline`, new findings), 2 usage.
|
|
96
|
+
|
|
97
|
+
## Library
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
import { check, stamp, KINDS, judge, stampStandard } from '@supersuit/hypercontext';
|
|
101
|
+
const report = await check('/path/to/workspace');
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
MIT.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve, join } from 'node:path';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { check, stamp, KINDS, readBaseline, applyBaseline, writeBaseline, renderReport, renderStamp, exportManifests } from '../src/index.mjs';
|
|
5
|
+
|
|
6
|
+
const HELP = `hypercontext: one check over a whole workspace, for every kind of record in it
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
hypercontext check [<workspace>] [--baseline <file>] [--kind <k>[,<k>]] [--packages <dir>] [--no-shapes] [--online] [--json]
|
|
10
|
+
Every record names the version of the standard it follows (the hypercontext property), and
|
|
11
|
+
every kind's own package passes it. One row per kind. --online also proves every standard
|
|
12
|
+
the records name opens, and that its published manifest is the one its owner ships. Exit 1
|
|
13
|
+
on any error, or with --baseline on any error the baseline does not already hold.
|
|
14
|
+
hypercontext stamp [<workspace>] [--kind <k>[,<k>]] [--packages <dir>] [--apply] [--json]
|
|
15
|
+
Add the missing standard: to existing records, each kind through its own package's
|
|
16
|
+
backfill. A dry run unless --apply. Never commits.
|
|
17
|
+
hypercontext baseline [<workspace>] --write <file> [--accept] [--packages <dir>]
|
|
18
|
+
Record today's errors as known debt. Without --accept it only ever shrinks.
|
|
19
|
+
hypercontext kinds [--json]
|
|
20
|
+
Every kind, its standard and the package that checks it.
|
|
21
|
+
hypercontext manifests --out <dir> [--packages <dir>]
|
|
22
|
+
Copy every kind's manifest from the package that owns it into <dir>/<kind>/v<N>.json, plus
|
|
23
|
+
<dir>/index.json, for a site to publish unchanged. Exit 1 when an owner is not installed or ships
|
|
24
|
+
a manifest that is not valid (it is listed, never written).
|
|
25
|
+
hypercontext --help | --version
|
|
26
|
+
|
|
27
|
+
<workspace> defaults to the current folder. Each kind's package is found by node resolution, then
|
|
28
|
+
beside this package in the hypercontext repository. --packages names a folder of package checkouts
|
|
29
|
+
to use instead, and only those (also HYPERCONTEXT_PACKAGES).
|
|
30
|
+
|
|
31
|
+
Exit codes: 0 clean, 1 findings, 2 usage.`;
|
|
32
|
+
|
|
33
|
+
const argv = process.argv.slice(2);
|
|
34
|
+
const cmd = argv[0];
|
|
35
|
+
const flag = (n) => argv.includes(n);
|
|
36
|
+
const opt = (n) => { const i = argv.indexOf(n); return i >= 0 ? argv[i + 1] : undefined; };
|
|
37
|
+
const valued = new Set(['--baseline', '--kind', '--packages', '--write', '--out']);
|
|
38
|
+
const positional = argv.slice(1).filter((a, i, all) => !a.startsWith('--') && !valued.has(all[i - 1]));
|
|
39
|
+
const ws = resolve(positional[0] ?? '.');
|
|
40
|
+
const json = flag('--json');
|
|
41
|
+
const kinds = opt('--kind')?.split(',').map((s) => s.trim()).filter(Boolean);
|
|
42
|
+
const packagesDir = opt('--packages') ? resolve(opt('--packages')) : undefined;
|
|
43
|
+
|
|
44
|
+
const usage = (m) => { process.stderr.write(`${m}\nRun hypercontext --help for usage.\n`); process.exit(2); };
|
|
45
|
+
// Write, then exit once the pipe has taken all of it. process.exit() straight after a large
|
|
46
|
+
// write to a pipe cuts the output off mid-stream, and a cut-off --json is a parse error for
|
|
47
|
+
// whoever is reading it.
|
|
48
|
+
const finish = (text, code) => process.stdout.write(text, () => process.exit(code));
|
|
49
|
+
|
|
50
|
+
if (!cmd || cmd === '--help' || cmd === '-h') { process.stdout.write(HELP + '\n'); process.exit(cmd ? 0 : 2); }
|
|
51
|
+
if (cmd === '--version') { process.stdout.write(JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version + '\n'); process.exit(0); }
|
|
52
|
+
if (kinds) for (const k of kinds) if (!KINDS.some((x) => x.kind === k)) usage(`no such kind: ${k} (hypercontext kinds lists them)`);
|
|
53
|
+
|
|
54
|
+
if (cmd === 'kinds') {
|
|
55
|
+
const rows = KINDS.map((k) => ({ kind: k.kind, standard: k.standard, package: k.pkg?.name ?? null }));
|
|
56
|
+
finish(json ? JSON.stringify(rows, null, 2) + '\n' : rows.map((r) => `${r.kind.padEnd(18)} ${r.standard} ${r.package ?? '(no package yet)'}`).join('\n') + '\n', 0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (cmd === 'check') {
|
|
60
|
+
const baseFile = opt('--baseline') ? resolve(opt('--baseline')) : undefined;
|
|
61
|
+
let baseline = null;
|
|
62
|
+
try { baseline = readBaseline(baseFile); } catch (e) { usage(`cannot read the baseline ${baseFile}: ${e.message}`); }
|
|
63
|
+
if (baseFile && !baseline) usage(`no baseline at ${baseFile}; write one with hypercontext baseline --write`);
|
|
64
|
+
const r = applyBaseline(await check(ws, { kinds, packagesDir, shapes: !flag('--no-shapes'), online: flag('--online') }), baseline);
|
|
65
|
+
finish(json ? JSON.stringify(r, null, 2) + '\n' : renderReport(r) + '\n', r.ok ? 0 : 1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (cmd === 'stamp') {
|
|
69
|
+
const s = await stamp(ws, { kinds, apply: flag('--apply'), packagesDir });
|
|
70
|
+
finish(json ? JSON.stringify(s, null, 2) + '\n' : renderStamp(s) + '\n', 0);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (cmd === 'baseline') {
|
|
74
|
+
const file = opt('--write');
|
|
75
|
+
if (!file) usage('baseline needs --write <file>');
|
|
76
|
+
const r = await check(ws, { kinds, packagesDir });
|
|
77
|
+
const w = writeBaseline(resolve(file), r, { accept: flag('--accept') });
|
|
78
|
+
finish(json ? JSON.stringify(w) + '\n' : `baseline: ${w.count} known error(s) written to ${file}; ${w.dropped} fixed since the last one${w.refused ? `; ${w.refused} new error(s) NOT accepted (pass --accept to record them)` : ''}\n`, w.refused ? 1 : 0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (cmd === 'manifests') {
|
|
82
|
+
const out = opt('--out');
|
|
83
|
+
if (!out) usage('manifests needs --out <dir>');
|
|
84
|
+
const index = exportManifests(resolve(out), { packagesDir });
|
|
85
|
+
const bad = index.filter((x) => x.missing || x.invalid);
|
|
86
|
+
const line = (x) => (x.missing ? `MISSING: ${x.owner} is not installed` : x.invalid ? `INVALID: ${x.invalid.join(' | ')}` : x.versions.join(' '));
|
|
87
|
+
finish(json ? JSON.stringify(index, null, 2) + '\n' : index.map((x) => `${x.kind.padEnd(18)} ${line(x)}`).join('\n') + '\n', bad.length ? 1 : 0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// finish() exits only once the output has drained, so nothing below may run after a command.
|
|
91
|
+
if (!['kinds', 'check', 'stamp', 'baseline', 'manifests'].includes(cmd)) usage(`unknown command: ${cmd}`);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Standards: versioned links, and the manifest behind each one
|
|
2
|
+
|
|
3
|
+
Gary, 2026-10-20: *"I'm not really tied to any past way of engineering the standards and I want the best possible airtight, validatable, hyperlinked hypercontext thing."*
|
|
4
|
+
|
|
5
|
+
## 1. The link names the version
|
|
6
|
+
|
|
7
|
+
Every record's header carries the version of the standard it was written to:
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
standard: "https://hypercontext.wiki/standards/friend/v1"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- `https://hypercontext.wiki/standards/<kind>/v<N>` is one version of one standard. **Its meaning never changes once published.** A correction that changes what a valid record is becomes `v<N+1>`; a typo fix to the prose does not.
|
|
14
|
+
- `https://hypercontext.wiki/standards/<kind>` is the latest version, and lists every version with what changed.
|
|
15
|
+
- A record names a version, never the bare `/standards/<kind>` URL. A record that does is a record written before versions existed: still that kind, no longer self-documenting, and its package's stamp upgrades it in place.
|
|
16
|
+
|
|
17
|
+
This is [every link says which version it meant](https://hypercontext.wiki/every-link-says-which-version-it-meant) applied to the link every record carries: a standard is a link to what a thing *said*, so it is pinned.
|
|
18
|
+
|
|
19
|
+
## 2. Every version has a manifest
|
|
20
|
+
|
|
21
|
+
Beside each version page is its machine-readable twin, `https://hypercontext.wiki/standards/<kind>/v<N>.json`. It says, as data, everything a reader needs to validate a record without installing anything, and everything a checker needs to prove the link it trusts says what it enforces.
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"manifest_version": 1,
|
|
26
|
+
"standard": "https://hypercontext.wiki/standards/friend/v1",
|
|
27
|
+
"kind": "friend",
|
|
28
|
+
"version": 1,
|
|
29
|
+
"title": "A person",
|
|
30
|
+
"status": "current",
|
|
31
|
+
"superseded_by": null,
|
|
32
|
+
"latest": "https://hypercontext.wiki/standards/friend",
|
|
33
|
+
"record": { "path": "people/<slug>/friend.md", "format": "md" },
|
|
34
|
+
"header": {
|
|
35
|
+
"required": ["kind", "standard", "id", "name", "summary", "read_when", "as_of", "keep"],
|
|
36
|
+
"keys": {
|
|
37
|
+
"kind": { "const": "friend" },
|
|
38
|
+
"standard": { "const": "https://hypercontext.wiki/standards/friend/v1" },
|
|
39
|
+
"keep": { "enum": ["keep", "home", "share"] }
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"validator": {
|
|
43
|
+
"package": "@supersuit/friend-md",
|
|
44
|
+
"implements_since": "0.3.0",
|
|
45
|
+
"check": "npx -y @supersuit/friend-md@^0.3.0 lint --people people --json",
|
|
46
|
+
"backfill": "npx -y @supersuit/friend-md@^0.3.0 stamp --people people --apply"
|
|
47
|
+
},
|
|
48
|
+
"codes": [
|
|
49
|
+
{ "code": "self-doc:missing", "level": "error", "means": "the record names no standard, an older one, or another kind's", "fix": "friend-md stamp --people people --apply" }
|
|
50
|
+
],
|
|
51
|
+
"umbrella": "npx -y @supersuit/hypercontext check <workspace>"
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
| key | meaning |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `manifest_version` | `1`, the shape of this file |
|
|
58
|
+
| `standard` | this version's URL; equals the `standard:` a record carries |
|
|
59
|
+
| `kind`, `version` | the record kind and this version's number |
|
|
60
|
+
| `title` | what a person calls the record |
|
|
61
|
+
| `status`, `superseded_by` | `current` or `superseded`, and the URL that replaced it |
|
|
62
|
+
| `latest` | the unversioned page that always shows the newest version |
|
|
63
|
+
| `record.path`, `record.format` | where records of this kind live, and `md` (YAML header) or `json` |
|
|
64
|
+
| `header.required`, `header.keys` | the header keys a record must carry, and the constraints a reader can check without a package, with JSON Schema's meaning (`const`, `enum`, `pattern`, `type`, `items`, `minLength`, `maxLength`, `minItems`, `maxItems`); `@supersuit/hypercontext-core`'s `validateManifest` refuses any other |
|
|
65
|
+
| `validator` | the package that checks the full shape, the first version of it that implements this standard version, and the exact commands; `null` when no package exists yet |
|
|
66
|
+
| `checked_by` | only when `validator` is `null`: the umbrella command that checks the shared property, the backfill when one may run, and a `note` saying what is not checked |
|
|
67
|
+
| `codes` | **every** finding the validator can report, with its level, what it means and how to fix it. Equal to the package's own code table, which a test in the package proves |
|
|
68
|
+
| `umbrella` | the one command that checks every kind |
|
|
69
|
+
|
|
70
|
+
## 3. One source, no copies that drift
|
|
71
|
+
|
|
72
|
+
**The package owns the manifest.** Each record package ships `standards/<kind>/v<N>.json` in its tarball, exports it, and its test suite asserts that the manifest's `codes` are exactly the codes its lint can emit and that `standard` equals its exported `STANDARD`. The wiki publishes the package's file unchanged. A kind with no package yet gets its manifest from `@supersuit/hypercontext`, with `validator: null`.
|
|
73
|
+
|
|
74
|
+
## 4. What the checks prove
|
|
75
|
+
|
|
76
|
+
`hypercontext check` refuses, as `self-doc:missing`:
|
|
77
|
+
|
|
78
|
+
- a record naming no standard;
|
|
79
|
+
- a record naming the bare, unversioned URL of its kind (written before versions; `stamp` upgrades it);
|
|
80
|
+
- a record naming another kind's standard;
|
|
81
|
+
|
|
82
|
+
and, as `self-doc:version`, a record naming a version of its kind that the installed validator does not implement (from the future, or retired without a migration).
|
|
83
|
+
|
|
84
|
+
It also refuses, as `manifest:invalid`, any manifest an installed owner ships that does not have the shape in section 2 (checked by `validateManifest` in `@supersuit/hypercontext-core`), or whose `kind` and `version` disagree with its path. `hypercontext manifests` never exports such a file.
|
|
85
|
+
|
|
86
|
+
`hypercontext check --online` also fetches every distinct standard a workspace's records name, and refuses (`standard:unreachable`) any that does not answer, and (`standard:drift`) any whose published manifest differs from the one the installed package ships. That is the hyperlink half of airtight: every link a record carries opens, and opens onto the rules the checker actually enforced.
|
package/package.json
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supersuit/hypercontext",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "One check over a whole workspace: every record names the standard it follows, and every kind of record passes its own package's checks.",
|
|
5
|
+
"keywords": ["hypercontext", "records", "markdown", "frontmatter", "lint", "self-documenting", "agents", "supersuit"],
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": { "hypercontext": "bin/hypercontext.mjs" },
|
|
8
|
+
"exports": { ".": "./src/index.mjs", "./package.json": "./package.json" },
|
|
9
|
+
"files": ["bin/", "src/", "docs/", "README.md", "CHANGELOG.md", "LICENSE"],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node --test test/*.test.mjs",
|
|
12
|
+
"mutation-check": "node scripts/mutation-check.mjs"
|
|
13
|
+
},
|
|
14
|
+
"engines": { "node": ">=20" },
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"publishConfig": { "access": "public" },
|
|
17
|
+
"repository": { "type": "git", "url": "git+https://github.com/SupersuitUp/hypercontext.git", "directory": "packages/hypercontext" },
|
|
18
|
+
"dependencies": { "@supersuit/hypercontext-core": "^0.1.0" }
|
|
19
|
+
}
|
package/src/baseline.mjs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// The ratchet: debt a workspace already had is recorded and tolerated; new debt fails.
|
|
2
|
+
//
|
|
3
|
+
// A workspace adopting a standard holds thousands of records written before it, and a check that
|
|
4
|
+
// is red until every one is fixed is a check nobody runs. So the baseline lists the error
|
|
5
|
+
// findings that existed when it was written, and `check --baseline` fails only on an error the
|
|
6
|
+
// list does not hold. The list only ever shrinks by itself: a finding that is gone is dropped the
|
|
7
|
+
// next time the baseline is written, and writing it never adds an error that a check had already
|
|
8
|
+
// refused, unless the person writing it says so with --accept.
|
|
9
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
10
|
+
|
|
11
|
+
export const keyOf = (f) => `${f.kind}|${f.code}|${f.path}`;
|
|
12
|
+
|
|
13
|
+
export function readBaseline(file) {
|
|
14
|
+
if (!file || !existsSync(file)) return null;
|
|
15
|
+
const j = JSON.parse(readFileSync(file, 'utf8'));
|
|
16
|
+
return { file, known: new Set(Array.isArray(j.known) ? j.known : []), written: j.written ?? '' };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Split a report's errors into the ones the baseline already knew and the new ones. */
|
|
20
|
+
export function applyBaseline(report, baseline) {
|
|
21
|
+
if (!baseline) return { ...report, known: 0, fresh: report.errors };
|
|
22
|
+
let known = 0;
|
|
23
|
+
const findings = report.findings.map((f) => {
|
|
24
|
+
if (f.level !== 'error') return f;
|
|
25
|
+
if (baseline.known.has(keyOf(f))) { known++; return { ...f, known: true }; }
|
|
26
|
+
return f;
|
|
27
|
+
});
|
|
28
|
+
const fresh = report.errors - known;
|
|
29
|
+
return { ...report, findings, known, fresh, ok: fresh === 0 };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Write the baseline from a report. With an existing baseline and no `accept`, it keeps only the
|
|
34
|
+
* errors still present that it already knew, so new debt cannot be written in by accident.
|
|
35
|
+
*/
|
|
36
|
+
export function writeBaseline(file, report, { accept = false, today = new Date().toISOString().slice(0, 10) } = {}) {
|
|
37
|
+
const prior = readBaseline(file);
|
|
38
|
+
// One entry per record and code: two findings on one record are one piece of debt.
|
|
39
|
+
const now = [...new Set(report.findings.filter((f) => f.level === 'error').map(keyOf))];
|
|
40
|
+
const known = prior && !accept ? now.filter((k) => prior.known.has(k)) : now;
|
|
41
|
+
const sorted = [...known].sort();
|
|
42
|
+
writeFileSync(file, JSON.stringify({
|
|
43
|
+
about: 'Error findings @supersuit/hypercontext tolerates in this workspace because they predate the check. Each fixed one drops out the next time this is written; a new one fails the check. https://hypercontext.wiki/standards',
|
|
44
|
+
written: today,
|
|
45
|
+
count: sorted.length,
|
|
46
|
+
known: sorted,
|
|
47
|
+
}, null, 2) + '\n');
|
|
48
|
+
return { count: sorted.length, dropped: prior ? [...prior.known].filter((k) => !sorted.includes(k)).length : 0, refused: now.length - sorted.length };
|
|
49
|
+
}
|
package/src/check.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// One check over a whole workspace: every kind, the shared property, and each kind's own package.
|
|
2
|
+
//
|
|
3
|
+
// The report is a row per kind (how many records, how many self-documenting, how many carry the
|
|
4
|
+
// shared context, what checked the shape) plus the findings. An error fails the check. A kind
|
|
5
|
+
// with no package is a warning, once, rather than silence: "nothing checks this" is the finding
|
|
6
|
+
// that a check reporting only on what it checks can never make.
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { KINDS } from './kinds.mjs';
|
|
10
|
+
import { findRecords, legacyRecords, superprojectRoots, documentHomes, fsDefault } from './find.mjs';
|
|
11
|
+
import { judge, acceptedFor, standardOf, CODE, VERSION_CODE } from './selfdoc.mjs';
|
|
12
|
+
import { implementedVersions, checkOnline, manifestFindings } from './manifest.mjs';
|
|
13
|
+
import { resolvePackage, packageVersion, LINTS } from './packages.mjs';
|
|
14
|
+
|
|
15
|
+
// Codes a package reports for the property this package judges itself. Dropped from the package's
|
|
16
|
+
// findings so one missing field is one finding, counted once, in one place.
|
|
17
|
+
const SELF_DOC_CODES = new Set([CODE, VERSION_CODE, 'standard:missing', 'standard:value']);
|
|
18
|
+
|
|
19
|
+
const STANDARDS = Object.fromEntries(KINDS.map((k) => [k.kind, k.standard]));
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} workspace absolute path
|
|
23
|
+
* @param {object} [opts]
|
|
24
|
+
* @param {string[]} [opts.kinds] limit to these kinds
|
|
25
|
+
* @param {string} [opts.packagesDir] a folder of sibling package checkouts (see packages.mjs)
|
|
26
|
+
* @param {boolean} [opts.shapes=true] false skips the packages and judges only the shared property
|
|
27
|
+
* @param {boolean} [opts.online=false] also prove every standard the records name is live and matches its owner's manifest
|
|
28
|
+
* @param {Function} [opts.fetch] injected fetch for tests
|
|
29
|
+
* @param {object} [opts.fs] injected filesystem for tests
|
|
30
|
+
* @param {object[]} [opts.registry] the kinds to judge, KINDS unless a test supplies its own
|
|
31
|
+
*/
|
|
32
|
+
export async function check(workspace, { kinds, packagesDir, shapes = true, online = false, fetch, fs = fsDefault, read = (p) => readFileSync(p, 'utf8'), registry = KINDS } = {}) {
|
|
33
|
+
const wanted = registry.filter((k) => !kinds || kinds.includes(k.kind));
|
|
34
|
+
const rows = [];
|
|
35
|
+
const findings = [];
|
|
36
|
+
const seen = new Map(); // kind -> Set of standard URLs its records name
|
|
37
|
+
|
|
38
|
+
for (const k of wanted) {
|
|
39
|
+
const records = findRecords(workspace, k.kind, fs);
|
|
40
|
+
let selfDoc = 0, context = 0;
|
|
41
|
+
const versions = implementedVersions(k, { packagesDir });
|
|
42
|
+
findings.push(...manifestFindings(k, { packagesDir }));
|
|
43
|
+
const named = new Set();
|
|
44
|
+
seen.set(k.kind, named);
|
|
45
|
+
for (const rel of records) {
|
|
46
|
+
let text;
|
|
47
|
+
try { text = read(join(workspace, rel)); } catch { findings.push({ kind: k.kind, level: 'error', code: 'record:unreadable', path: rel, message: 'cannot be read' }); continue; }
|
|
48
|
+
const got = standardOf(text, k.format);
|
|
49
|
+
const verdict = judge(text, k, { versions, alsoAccept: acceptedFor(k.kind, got.data, STANDARDS) });
|
|
50
|
+
if (got.standard) named.add(got.standard);
|
|
51
|
+
if (verdict.selfDocumenting) selfDoc++;
|
|
52
|
+
else findings.push({ kind: k.kind, level: 'error', code: verdict.code, path: rel, message: verdict.problem });
|
|
53
|
+
if (k.context.length === 0 || verdict.contextComplete) context++;
|
|
54
|
+
}
|
|
55
|
+
const row = { kind: k.kind, label: k.label, standard: k.standard, records: records.length, selfDocumenting: selfDoc, contextComplete: context, validator: null, validatorNote: '' };
|
|
56
|
+
|
|
57
|
+
if (!k.pkg) {
|
|
58
|
+
row.validatorNote = 'no package checks this shape yet';
|
|
59
|
+
if (records.length) findings.push({ kind: k.kind, level: 'warn', code: 'validator:missing', path: '', message: `${records.length} ${k.kind} record(s), and no package checks their shape; only the shared property is checked` });
|
|
60
|
+
} else if (!shapes) {
|
|
61
|
+
row.validatorNote = 'shapes skipped';
|
|
62
|
+
} else if (k.sharesLint) {
|
|
63
|
+
row.validator = k.pkg.name;
|
|
64
|
+
row.validatorNote = `checked with ${k.sharesLint}`;
|
|
65
|
+
} else if (records.length === 0 && !registry.some((x) => x.sharesLint === k.kind && findRecords(workspace, x.kind, fs).length)) {
|
|
66
|
+
// A kind whose run also checks another (hdsop checks the catalog) still runs when only the
|
|
67
|
+
// other kind has records.
|
|
68
|
+
row.validator = k.pkg.name;
|
|
69
|
+
row.validatorNote = 'nothing to check';
|
|
70
|
+
} else {
|
|
71
|
+
const dir = resolvePackage(k.pkg, { packagesDir });
|
|
72
|
+
if (!dir) {
|
|
73
|
+
row.validatorNote = `${k.pkg.name} is not installed`;
|
|
74
|
+
findings.push({ kind: k.kind, level: 'warn', code: 'validator:absent', path: '', message: `${k.pkg.name} checks ${k.kind} records and is not installed here; install it, or pass --packages <dir of checkouts>` });
|
|
75
|
+
} else {
|
|
76
|
+
const ctx = {
|
|
77
|
+
roots: superprojectRoots(workspace, fs),
|
|
78
|
+
homes: k.kind === 'superobjective'
|
|
79
|
+
? [...new Set(records.map((r) => r.split('/').slice(0, -2).join('/')))].filter(Boolean)
|
|
80
|
+
: documentHomes(workspace, fs),
|
|
81
|
+
records,
|
|
82
|
+
read: (p) => readFileSync(p),
|
|
83
|
+
};
|
|
84
|
+
const res = await LINTS[k.kind](dir, k.pkg.bin, workspace, ctx);
|
|
85
|
+
row.validator = `${k.pkg.name}@${packageVersion(dir)}`;
|
|
86
|
+
if (!res.ran) {
|
|
87
|
+
row.validatorNote = `could not run: ${res.note}`;
|
|
88
|
+
findings.push({ kind: k.kind, level: 'error', code: 'validator:failed', path: '', message: `${k.pkg.name} did not run: ${res.note}` });
|
|
89
|
+
} else {
|
|
90
|
+
for (const f of res.findings) {
|
|
91
|
+
if (SELF_DOC_CODES.has(f.code) || f.level === 'ok' || f.level === 'info') continue;
|
|
92
|
+
findings.push({ kind: k.kind, ...f });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
rows.push(row);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const l of legacyRecords(workspace, fs)) {
|
|
101
|
+
if (kinds && !kinds.includes(l.kind)) continue;
|
|
102
|
+
findings.push({ kind: l.kind, level: 'warn', code: 'record:legacy-shape', path: l.path, message: `a ${l.kind} kept in a shape its standard retired; its package's migrate moves it` });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (online) {
|
|
106
|
+
// Only this workspace's own kinds' URLs: a document naming the piece standard is checked as a piece.
|
|
107
|
+
for (const [kind, urls] of seen) for (const u of [...urls]) if (!u.startsWith(`https://hypercontext.wiki/standards/${kind}`)) urls.delete(u);
|
|
108
|
+
findings.push(...await checkOnline(seen, { fetch, packagesDir }));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const errors = findings.filter((f) => f.level === 'error').length;
|
|
112
|
+
const warnings = findings.filter((f) => f.level === 'warn').length;
|
|
113
|
+
return { workspace, ok: errors === 0, errors, warnings, rows, findings };
|
|
114
|
+
}
|